[fix] User have no firstName nor lastName (#16057)

Closes https://github.com/twentyhq/twenty/issues/15692, fixes
https://github.com/twentyhq/twenty/issues/14678

The issue was:
- When a user is signing up, they are asked for their first name and
last name with which we fulfill their workspaceMember entity for the
workspace they are signing up to (which is the one they are creating if
they are not joining an existing workspace). user.firstName and
user.lastName remain empty
- When we create a record, we are using user.firstName and user.lastName
to fulfill the text field "createdByName", which intends to save the
name of the creator at the moment of the creation. This field is then
use 1) when filtering on createdBy (as a trick to be able to filter by
this, since we don't have filters on nested fields, ie we dont have
filters on person.createdByWorkspaceMember) 2) to display the user
creator when a user has left a workspace - otherwise we rely on the
dynamic createdByWorkspaceMember.firstName and lastName
- So filtering on createdBy was not working as createdByName is empty.
We did not see that in dev mode because we have seeded users with names

The fix
- When we create a record, we use workspaceMember.firstName and
workspaceMember.lastName as we should.
- When an existing user joins a workspace, they are asked to give their
name again so that we set their workspaceMember name
- This will not fix the history of the records (they will still have
createdByName empty so the filter will not work properly), but a command
to fix it would be very long as it would iterate over all the records of
each active workspace

The long-term view is to get rid of user and to store the name in
userWorkspace
This commit is contained in:
Marie
2025-11-25 18:23:43 +01:00
committed by GitHub
parent b1c03b533f
commit 6c1c78ea3d
7 changed files with 52 additions and 18 deletions
@@ -64,5 +64,5 @@ export const useSignInUpForm = () => {
form.setValue('password', 'tim@apple.dev');
}
}, [form, isDeveloperDefaultSignInPrefilled, prefilledEmail]);
return { form: form };
return { form };
};
@@ -106,13 +106,25 @@ describe('CreatedByFromAuthContextService', () => {
const authContext = {
workspaceMemberId: '20202020-0b5c-4178-bed7-d371f6411eaa',
user: {
firstName: 'John',
lastName: 'Doe',
firstName: '',
lastName: '',
id: '20202020-9aae-49a8-bafc-ac44bae62d6d',
},
workspace: { id: '20202020-bdec-497f-847a-1bb334fefe58' },
} as const satisfies TestingAuthContext;
const mockedWorkspaceMember = {
id: '20202020-0b5c-4178-bed7-d371f6411eaa',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as const satisfies Partial<WorkspaceMemberWorkspaceEntity>;
mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValueOnce(
mockedWorkspaceMember,
);
const result = await service.injectCreatedBy(
[{}],
'person',
@@ -123,7 +135,7 @@ describe('CreatedByFromAuthContextService', () => {
{
createdBy: {
context: {},
name: fromFullNameMetadataToName(authContext.user),
name: fromFullNameMetadataToName(mockedWorkspaceMember.name),
workspaceMemberId: authContext.workspaceMemberId,
source: FieldActorSource.MANUAL,
},
@@ -102,24 +102,11 @@ export class CreatedByFromAuthContextService {
private async buildCreatedBy(
authContext: AuthContext,
): Promise<ActorMetadata> {
const { workspace, workspaceMemberId, user, apiKey } = authContext;
const { workspace, user, apiKey } = authContext;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
// TODO: remove that code once we have the workspace member id in all tokens
if (isDefined(workspaceMemberId) && isDefined(user)) {
return buildCreatedByFromFullNameMetadata({
fullNameMetadata: {
firstName: user.firstName,
lastName: user.lastName,
},
workspaceMemberId,
});
}
if (isDefined(user)) {
this.logger.warn("User doesn't have a workspace member id in the token");
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspace.id,
@@ -9,6 +9,7 @@ import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -43,6 +44,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
FileUploadModule,
FileModule,
TokenModule,
OnboardingModule,
],
services: [UserWorkspaceService],
}),
@@ -16,6 +16,7 @@ import {
type SignedFilesResult,
} from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -38,6 +39,7 @@ describe('UserWorkspaceService', () => {
let userRoleService: UserRoleService;
let fileService: FileService;
let fileUploadService: FileUploadService;
let onboardingService: OnboardingService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -133,6 +135,12 @@ describe('UserWorkspaceService', () => {
copyFileFromWorkspaceToWorkspace: jest.fn(),
},
},
{
provide: OnboardingService,
useValue: {
setOnboardingCreateProfilePending: jest.fn(),
},
},
],
}).compile();
@@ -153,6 +161,7 @@ describe('UserWorkspaceService', () => {
);
userRoleService = module.get<UserRoleService>(UserRoleService);
fileUploadService = module.get<FileUploadService>(FileUploadService);
onboardingService = module.get<OnboardingService>(OnboardingService);
});
it('should be defined', () => {
@@ -444,6 +453,17 @@ describe('UserWorkspaceService', () => {
expect(
workspaceInvitationService.invalidateWorkspaceInvitation,
).toHaveBeenCalledWith(workspace.id, user.email, undefined);
expect(
onboardingService.setOnboardingCreateProfilePending,
).toHaveBeenCalledWith(
{
userId: user.id,
workspaceId: workspace.id,
value: true,
},
undefined,
);
});
it('should not add user to workspace if already in workspace', async () => {
@@ -19,6 +19,7 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
@@ -53,6 +54,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
private readonly userRoleService: UserRoleService,
private readonly fileUploadService: FileUploadService,
private readonly fileService: FileService,
private readonly onboardingService: OnboardingService,
) {
super(userWorkspaceRepository);
}
@@ -173,6 +175,15 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
user.email,
queryRunner,
);
await this.onboardingService.setOnboardingCreateProfilePending(
{
userId: user.id,
workspaceId: workspace.id,
value: true,
},
queryRunner,
);
}
}
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMemberCreateManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-create-many.pre-query.hook';
import { WorkspaceMemberCreateOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-create-one.pre-query.hook';
@@ -38,6 +39,7 @@ import { WorkspaceMemberUpdateOnePreQueryHook } from 'src/modules/workspace-memb
PermissionsModule,
UserWorkspaceModule,
TypeOrmModule.forFeature([UserWorkspaceEntity]),
UserModule,
],
})
export class WorkspaceMemberQueryHookModule {}