diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 03f96f97e..ed38f848b 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -591,6 +591,15 @@ export type DeleteWorkflowVersionStepInput = { workflowVersionId: Scalars['String']['input']; }; +export type DeletedWorkspaceMember = { + __typename?: 'DeletedWorkspaceMember'; + avatarUrl?: Maybe; + id: Scalars['UUID']['output']; + name: FullName; + userEmail: Scalars['String']['output']; + userWorkspaceId?: Maybe; +}; + /** Schema update on a table */ export enum DistantTableUpdate { COLUMNS_ADDED = 'COLUMNS_ADDED', @@ -2426,6 +2435,7 @@ export type User = { currentWorkspace?: Maybe; defaultAvatarUrl?: Maybe; deletedAt?: Maybe; + deletedWorkspaceMembers?: Maybe>; disabled?: Maybe; email: Scalars['String']['output']; firstName: Scalars['String']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 2f1c8176f..d245f3e07 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -522,6 +522,15 @@ export type DeleteWorkflowVersionStepInput = { workflowVersionId: Scalars['String']; }; +export type DeletedWorkspaceMember = { + __typename?: 'DeletedWorkspaceMember'; + avatarUrl?: Maybe; + id: Scalars['UUID']; + name: FullName; + userEmail: Scalars['String']; + userWorkspaceId?: Maybe; +}; + /** Schema update on a table */ export enum DistantTableUpdate { COLUMNS_ADDED = 'COLUMNS_ADDED', @@ -2213,6 +2222,7 @@ export type User = { currentWorkspace?: Maybe; defaultAvatarUrl?: Maybe; deletedAt?: Maybe; + deletedWorkspaceMembers?: Maybe>; disabled?: Maybe; email: Scalars['String']; firstName: Scalars['String']; @@ -2915,7 +2925,7 @@ export type OnDbEventSubscriptionVariables = Exact<{ export type OnDbEventSubscription = { __typename?: 'Subscription', onDbEvent: { __typename?: 'OnDbEventDTO', eventDate: string, action: DatabaseEventAction, objectNameSingular: string, updatedFields?: Array | 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, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, 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, 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 | null, objectRecordsPermissions?: Array | 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, 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 DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2932,7 +2942,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, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array | null, objectRecordsPermissions?: Array | 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, 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, 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 | null, objectRecordsPermissions?: Array | 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, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, 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 }>, 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 ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -3030,6 +3040,8 @@ export type GetWorkspaceInvitationsQueryVariables = Exact<{ [key: string]: never export type GetWorkspaceInvitationsQuery = { __typename?: 'Query', findWorkspaceInvitations: Array<{ __typename?: 'WorkspaceInvitation', id: any, email: string, expiresAt: string }> }; +export type DeletedWorkspaceMemberQueryFragmentFragment = { __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }; + export type WorkspaceMemberQueryFragmentFragment = { __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 } }; export type ActivateWorkspaceMutationVariables = Exact<{ @@ -3203,6 +3215,17 @@ export const WorkspaceMemberQueryFragmentFragmentDoc = gql` timeFormat } `; +export const DeletedWorkspaceMemberQueryFragmentFragmentDoc = gql` + fragment DeletedWorkspaceMemberQueryFragment on DeletedWorkspaceMember { + id + name { + firstName + lastName + } + avatarUrl + userEmail +} + `; export const RoleFragmentFragmentDoc = gql` fragment RoleFragment on Role { id @@ -3233,6 +3256,9 @@ export const UserQueryFragmentFragmentDoc = gql` workspaceMembers { ...WorkspaceMemberQueryFragment } + deletedWorkspaceMembers { + ...DeletedWorkspaceMemberQueryFragment + } currentUserWorkspace { settingsPermissions objectRecordsPermissions @@ -3304,6 +3330,7 @@ export const UserQueryFragmentFragmentDoc = gql` userVars } ${WorkspaceMemberQueryFragmentFragmentDoc} +${DeletedWorkspaceMemberQueryFragmentFragmentDoc} ${RoleFragmentFragmentDoc}`; export const GetTimelineCalendarEventsFromCompanyIdDocument = gql` query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) { diff --git a/packages/twenty-front/src/modules/auth/components/AuthProvider.tsx b/packages/twenty-front/src/modules/auth/components/AuthProvider.tsx index ce7c7bea7..c53256fb9 100644 --- a/packages/twenty-front/src/modules/auth/components/AuthProvider.tsx +++ b/packages/twenty-front/src/modules/auth/components/AuthProvider.tsx @@ -2,13 +2,22 @@ import React from 'react'; import { useRecoilValue } from 'recoil'; import { AuthContext } from '@/auth/contexts/AuthContext'; +import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersStates'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; export const AuthProvider = ({ children }: React.PropsWithChildren) => { const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState); + const currentWorkspaceDeletedMembers = useRecoilValue( + currentWorkspaceDeletedMembersState, + ); return ( - + {children} ); diff --git a/packages/twenty-front/src/modules/auth/contexts/AuthContext.ts b/packages/twenty-front/src/modules/auth/contexts/AuthContext.ts index f3cfac5f0..c01443c46 100644 --- a/packages/twenty-front/src/modules/auth/contexts/AuthContext.ts +++ b/packages/twenty-front/src/modules/auth/contexts/AuthContext.ts @@ -1,8 +1,10 @@ import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; import { createContext } from 'react'; +import { DeletedWorkspaceMember } from '~/generated-metadata/graphql'; export type AuthContextType = { currentWorkspaceMembers: CurrentWorkspaceMember[]; + currentWorkspaceDeletedMembers: DeletedWorkspaceMember[]; }; export const AuthContext = createContext( diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceDeletedMembersStates.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceDeletedMembersStates.ts new file mode 100644 index 000000000..bbce85ec4 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceDeletedMembersStates.ts @@ -0,0 +1,9 @@ +import { createState } from 'twenty-ui/utilities'; +import { DeletedWorkspaceMember } from '~/generated-metadata/graphql'; + +export const currentWorkspaceDeletedMembersState = createState< + DeletedWorkspaceMember[] +>({ + key: 'currentWorkspaceDeletedMembersState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ActorFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ActorFieldDisplay.tsx index cd176beb4..530b92b58 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ActorFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ActorFieldDisplay.tsx @@ -1,27 +1,22 @@ import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty'; import { useActorFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useActorFieldDisplay'; import { ActorDisplay } from '@/ui/field/display/components/ActorDisplay'; -import { isNonEmptyString } from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; export const ActorFieldDisplay = () => { - const { fieldValue } = useActorFieldDisplay(); - - const name = !fieldValue.workspaceMemberId - ? fieldValue.name - : [ - fieldValue.workspaceMember?.name.firstName, - fieldValue.workspaceMember?.name.lastName, - ] - .filter(isNonEmptyString) - .join(' '); + const actorFieldDisplay = useActorFieldDisplay(); const displayActorField = !useIsFieldEmpty(); + if (!isDefined(actorFieldDisplay)) { + return null; + } + const { fieldValue, name, avatarUrl } = actorFieldDisplay; return displayActorField ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ActorFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ActorFieldDisplay.perf.stories.tsx index ccd8b3c4e..09f0acdd4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ActorFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ActorFieldDisplay.perf.stories.tsx @@ -1,15 +1,17 @@ import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay'; import { Meta, StoryObj } from '@storybook/react'; -import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator'; -import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; -import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; -import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; import { ComponentDecorator } from 'twenty-ui/testing'; +import { AuthContextDecorator } from '~/testing/decorators/AuthContextDecorator'; +import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; +import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; const meta: Meta = { title: 'UI/Data/Field/Display/ActorFieldDisplay', decorators: [ + AuthContextDecorator, MemoryRouterDecorator, ChipGeneratorsDecorator, getFieldDecorator('company', 'createdBy', { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts index aa6378e13..e678ce180 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useActorFieldDisplay.ts @@ -4,12 +4,20 @@ import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadat import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { AuthContext } from '@/auth/contexts/AuthContext'; +import { isDefined } from 'twenty-shared/utils'; +import { WorkspaceMember } from '~/generated-metadata/graphql'; import { FieldContext } from '../../contexts/FieldContext'; -export const useActorFieldDisplay = () => { +export type ActorFieldDisplayValue = { + fieldValue: FieldActorValue; + name: string; +} & Pick; + +export const useActorFieldDisplay = (): ActorFieldDisplayValue | undefined => { const { recordId, fieldDefinition } = useContext(FieldContext); - const { currentWorkspaceMembers } = useContext(AuthContext); + const { currentWorkspaceDeletedMembers, currentWorkspaceMembers } = + useContext(AuthContext); const fieldName = fieldDefinition.metadata.fieldName; @@ -17,14 +25,27 @@ export const useActorFieldDisplay = () => { recordId, fieldName, ); + if (!isDefined(fieldValue)) { + return undefined; + } + const relatedWorkspaceMember = [ + ...currentWorkspaceDeletedMembers, + ...currentWorkspaceMembers, + ].find( + (workspaceMember) => workspaceMember.id === fieldValue.workspaceMemberId, + ); + if (!isDefined(relatedWorkspaceMember)) { + return { + fieldValue, + name: fieldValue.name, + }; + } + + const { name, avatarUrl } = relatedWorkspaceMember; return { - fieldDefinition, - fieldValue: { - ...fieldValue, - workspaceMember: currentWorkspaceMembers?.find( - (member) => member.id === fieldValue?.workspaceMemberId, - ), - }, + fieldValue, + name: `${name.firstName} ${name.lastName}`, + avatarUrl: avatarUrl, }; }; diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx index a8229c460..fbe225a99 100644 --- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx @@ -3,6 +3,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; +import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersStates'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; @@ -45,6 +46,9 @@ export const UserProviderEffect = () => { const setCurrentWorkspaceMembers = useSetRecoilState( currentWorkspaceMembersState, ); + const setCurrentWorkspaceMembersWithDeleted = useSetRecoilState( + currentWorkspaceDeletedMembersState, + ); const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({ skip: @@ -77,6 +81,7 @@ export const UserProviderEffect = () => { const { workspaceMember, workspaceMembers, + deletedWorkspaceMembers, workspaces: userWorkspaces, } = queryData.currentUser; @@ -122,6 +127,10 @@ export const UserProviderEffect = () => { ); } + if (isDefined(deletedWorkspaceMembers)) { + setCurrentWorkspaceMembersWithDeleted(deletedWorkspaceMembers); + } + if (isDefined(userWorkspaces)) { const workspaces = userWorkspaces .map(({ workspace }) => workspace) @@ -141,6 +150,7 @@ export const UserProviderEffect = () => { queryData?.currentUser, setIsCurrentUserLoaded, setDateTimeFormat, + setCurrentWorkspaceMembersWithDeleted, ]); return <>; diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index bf9ba8655..fe91bbbc4 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -1,4 +1,5 @@ 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'; @@ -19,6 +20,9 @@ export const USER_QUERY_FRAGMENT = gql` workspaceMembers { ...WorkspaceMemberQueryFragment } + deletedWorkspaceMembers { + ...DeletedWorkspaceMemberQueryFragment + } currentUserWorkspace { settingsPermissions objectRecordsPermissions @@ -91,4 +95,5 @@ export const USER_QUERY_FRAGMENT = gql` } ${WORKSPACE_MEMBER_QUERY_FRAGMENT} + ${DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT} `; diff --git a/packages/twenty-front/src/modules/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment.ts b/packages/twenty-front/src/modules/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment.ts new file mode 100644 index 000000000..f8c0c418f --- /dev/null +++ b/packages/twenty-front/src/modules/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; + +export const DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT = gql` + fragment DeletedWorkspaceMemberQueryFragment on DeletedWorkspaceMember { + id + name { + firstName + lastName + } + avatarUrl + userEmail + } +`; diff --git a/packages/twenty-front/src/testing/decorators/AuthContextDecorator.tsx b/packages/twenty-front/src/testing/decorators/AuthContextDecorator.tsx new file mode 100644 index 000000000..6bd381d07 --- /dev/null +++ b/packages/twenty-front/src/testing/decorators/AuthContextDecorator.tsx @@ -0,0 +1,16 @@ +import { Decorator } from '@storybook/react'; + +import { AuthContext } from '@/auth/contexts/AuthContext'; + +export const AuthContextDecorator: Decorator = (Story) => { + return ( + + + + ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/user/dtos/deleted-workspace-member.dto.ts b/packages/twenty-server/src/engine/core-modules/user/dtos/deleted-workspace-member.dto.ts new file mode 100644 index 000000000..a30239b84 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/user/dtos/deleted-workspace-member.dto.ts @@ -0,0 +1,24 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { FullName } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; + +@ObjectType() +export class DeletedWorkspaceMember { + @IDField(() => UUIDScalarType) + id: string; + + @Field(() => FullName) + name: FullName; + + @Field({ nullable: false }) + userEmail: string; + + @Field(() => String, { nullable: true }) + avatarUrl: string | null; + + @Field(() => String, { nullable: true }) + userWorkspaceId: string | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service.ts new file mode 100644 index 000000000..c2568cb0d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; + +import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@Injectable() +export class DeletedWorkspaceMemberTranspiler { + constructor(private readonly fileService: FileService) {} + + generateSignedAvatarUrl({ + workspaceId, + workspaceMember, + }: { + workspaceMember: Pick; + workspaceId: string; + }): string { + const avatarUrlToken = this.fileService.encodeFileToken({ + workspaceMemberId: workspaceMember.id, + workspaceId: workspaceId, + }); + + return `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`; + } + + toDeletedWorkspaceMemberDto( + workspaceMember: WorkspaceMemberWorkspaceEntity, + userWorkspaceId?: string, + ): DeletedWorkspaceMember { + const { + avatarUrl: avatarUrlFromEntity, + id, + name, + userEmail, + } = workspaceMember; + + const avatarUrl = userWorkspaceId + ? this.generateSignedAvatarUrl({ + workspaceId: userWorkspaceId, + workspaceMember: { + avatarUrl: avatarUrlFromEntity, + id, + }, + }) + : null; + + return { + id, + name, + userEmail, + avatarUrl, + userWorkspaceId: userWorkspaceId ?? null, + } satisfies DeletedWorkspaceMember; + } + + toDeletedWorkspaceMemberDtos( + workspaceMembers: WorkspaceMemberWorkspaceEntity[], + userWorkspaceId?: string, + ): DeletedWorkspaceMember[] { + return workspaceMembers.map((workspaceMember) => + this.toDeletedWorkspaceMemberDto(workspaceMember, userWorkspaceId), + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 8c248e4cc..7e0a0fbbe 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -4,7 +4,7 @@ import assert from 'assert'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; -import { Repository } from 'typeorm'; +import { IsNull, Not, Repository } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { @@ -63,7 +63,7 @@ export class UserService extends TypeOrmQueryService { }); } - async loadWorkspaceMembers(workspace: Workspace) { + async loadWorkspaceMembers(workspace: Workspace, withDeleted = false) { if (!isWorkspaceActiveOrSuspended(workspace)) { return []; } @@ -74,7 +74,24 @@ export class UserService extends TypeOrmQueryService { 'workspaceMember', ); - return workspaceMemberRepository.find(); + return await workspaceMemberRepository.find({ withDeleted: withDeleted }); + } + + async loadDeletedWorkspaceMembersOnly(workspace: Workspace) { + if (!isWorkspaceActiveOrSuspended(workspace)) { + return []; + } + + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspace.id, + 'workspaceMember', + ); + + return await workspaceMemberRepository.find({ + where: { deletedAt: Not(IsNull()) }, + withDeleted: true, + }); } private async deleteUserFromWorkspace({ diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 28ac87887..44d0cd725 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -24,6 +24,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; +import { DeletedWorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service'; import { userAutoResolverOpts } from './user.auto-resolver-opts'; @@ -53,7 +54,12 @@ import { UserService } from './services/user.service'; PermissionsModule, UserWorkspaceModule, ], - exports: [UserService], - providers: [UserService, UserResolver, TypeORMService], + exports: [UserService, DeletedWorkspaceMemberTranspiler], + providers: [ + UserService, + UserResolver, + TypeORMService, + DeletedWorkspaceMemberTranspiler, + ], }) export class UserModule {} diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index da764abb7..282f99e02 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -34,7 +34,9 @@ import { } from 'src/engine/core-modules/onboarding/onboarding.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; +import { DeletedWorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -79,6 +81,7 @@ export class UserResolver { private readonly userWorkspaceRepository: Repository, private readonly userRoleService: UserRoleService, private readonly permissionsService: PermissionsService, + private readonly deletedWorkspaceMemberTranspiler: DeletedWorkspaceMemberTranspiler, ) {} @Query(() => User) @@ -187,7 +190,7 @@ export class UserResolver { workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`; } - // TODO: Fix typing disrepency between Entity and DTO + // TODO Refactor to be transpiled to WorkspaceMember instead return workspaceMember as WorkspaceMember | null; } @@ -195,17 +198,15 @@ export class UserResolver { nullable: true, }) async workspaceMembers( - @Parent() user: User, + @Parent() _user: User, @AuthWorkspace() workspace: Workspace, ): Promise { - const workspaceMemberEntities = - await this.userService.loadWorkspaceMembers(workspace); + const workspaceMemberEntities = await this.userService.loadWorkspaceMembers( + workspace, + false, + ); const workspaceMembers: WorkspaceMember[] = []; - - let userWorkspacesByUserId = new Map(); - let rolesByUserWorkspaces = new Map(); - const userWorkspaces = await this.userWorkspaceRepository.find({ where: { userId: In(workspaceMemberEntities.map((entity) => entity.userId)), @@ -213,21 +214,20 @@ export class UserResolver { }, }); - userWorkspacesByUserId = new Map( + const userWorkspacesByUserId = new Map( userWorkspaces.map((userWorkspace) => [ userWorkspace.userId, userWorkspace, ]), ); - rolesByUserWorkspaces = await this.userRoleService.getRolesByUserWorkspaces( - { + const rolesByUserWorkspaces: Map = + await this.userRoleService.getRolesByUserWorkspaces({ userWorkspaceIds: userWorkspaces.map( (userWorkspace) => userWorkspace.id, ), workspaceId: workspace.id, - }, - ); + }); for (const workspaceMemberEntity of workspaceMemberEntities) { if (workspaceMemberEntity.avatarUrl) { @@ -239,12 +239,14 @@ export class UserResolver { workspaceMemberEntity.avatarUrl = `${workspaceMemberEntity.avatarUrl}?token=${avatarUrlToken}`; } + // TODO Refactor to be transpiled to WorkspaceMember instead const workspaceMember = workspaceMemberEntity as WorkspaceMember; const userWorkspace = userWorkspacesByUserId.get( workspaceMemberEntity.userId, ); + // TODO Refactor should not throw ? typed as nullable ? if (!userWorkspace) { throw new Error('User workspace not found'); } @@ -279,6 +281,22 @@ export class UserResolver { return workspaceMembers; } + @ResolveField(() => [DeletedWorkspaceMember], { + nullable: true, + }) + async deletedWorkspaceMembers( + @Parent() _user: User, + @AuthWorkspace() workspace: Workspace, + ): Promise { + const workspaceMemberEntities = + await this.userService.loadDeletedWorkspaceMembersOnly(workspace); + + return this.deletedWorkspaceMemberTranspiler.toDeletedWorkspaceMemberDtos( + workspaceMemberEntities, + workspace.id, + ); + } + @ResolveField(() => String, { nullable: true, })