Fix/workspace member avatars 20193 (#20200)
Fixes #20193 **Bug Description:** Previously, workspace member avatars failed to render correctly in table views and relation chips (such as the Account Owner field). While the avatar picker dropdown correctly fetched fresh GraphQL data, table views and chips relied on the cached defaultAvatarUrl or avatarUrl fields, which were frequently resolving to empty strings or failing to parse external OAuth URLs correctly. **Root Cause:** - Empty String Defaults: Deleting an avatar or failing to retrieve one defaulted the database state to an empty string ("") instead of null, which caused frontend image components to break rather than render their fallback states. - Missing Permanent URLs: The WorkspaceMemberTranspiler was strictly expecting internal signed URLs. If an avatar was an external OAuth URL, it incorrectly returned an empty string, breaking SSO profile pictures. - Missing Fallbacks: New users lacked a proper Gravatar fallback assignment upon workspace creation. **Changes Made:** - user-workspace.service.ts: Updated the avatar computation logic during user creation to implement a reliable Gravatar fallback and correctly set missing avatars to null instead of empty strings. Updated the storage to use permanent file URLs. - file-url.service.ts: Implemented a getRawFileUrl method to support rendering permanent, non-expiring file URLs for avatars. - workspace-member-transpiler.service.ts: Refactored the URL transpilation logic to gracefully pass through external OAuth URLs (e.g., Google/Microsoft profile pictures) instead of stripping them. - WorkspaceMemberPictureUploader.tsx: Fixed the frontend removal logic so that deleting a profile picture sets the avatarUrl to null (consistent with the backend) rather than an empty string. **Testing:** - Verified that avatars correctly display in relation chips and table views. - Verified that external OAuth avatars load properly. - Verified that deleting an avatar correctly resets the UI to the fallback initials component. Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.3.0', 1777915958318)
|
||||
export class RemoveUserDefaultAvatarUrlFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."user" DROP COLUMN "defaultAvatarUrl"',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."user" ADD "defaultAvatarUrl" character varying',
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -1,7 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillImageIdentifierFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777920000000-backfill-image-identifier-field-metadata-id.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [],
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [BackfillImageIdentifierFieldMetadataIdCommand],
|
||||
})
|
||||
export class V2_3_UpgradeVersionCommandModule {}
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const WORKSPACE_MEMBER_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.workspaceMember.universalIdentifier;
|
||||
|
||||
const AVATAR_URL_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.workspaceMember.fields.avatarUrl.universalIdentifier;
|
||||
|
||||
@RegisteredWorkspaceCommand('2.3.0', 1777920000000)
|
||||
@Command({
|
||||
name: 'upgrade:2-3:backfill-image-identifier-field-metadata-id',
|
||||
description:
|
||||
'Backfill imageIdentifierFieldMetadataId on workspaceMember for workspaces where it was never set.',
|
||||
})
|
||||
export class BackfillImageIdentifierFieldMetadataIdCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const existingObject =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: WORKSPACE_MEMBER_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
if (!isDefined(existingObject)) {
|
||||
this.logger.log(
|
||||
`workspaceMember object not found for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
existingObject.imageIdentifierFieldMetadataUniversalIdentifier ===
|
||||
AVATAR_URL_FIELD_UNIVERSAL_IDENTIFIER
|
||||
) {
|
||||
this.logger.log(
|
||||
`imageIdentifierFieldMetadataId already set for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const existingField =
|
||||
findFlatEntityByUniversalIdentifier<FlatFieldMetadata>({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier: AVATAR_URL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
if (!isDefined(existingField)) {
|
||||
this.logger.log(
|
||||
`avatarUrl field not found for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would backfill imageIdentifierFieldMetadataId on workspaceMember for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
objectMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [
|
||||
{
|
||||
...existingObject,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier:
|
||||
AVATAR_URL_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to backfill imageIdentifierFieldMetadataId for workspace ${workspaceId}:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to backfill imageIdentifierFieldMetadataId for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Backfilled imageIdentifierFieldMetadataId on workspaceMember for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -22,6 +22,7 @@ import { AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand } from 'src/data
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
import { AddDeletedAtToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777682000000-add-deleted-at-to-agent-chat-thread';
|
||||
import { RemoveUserDefaultAvatarUrlFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -46,4 +47,5 @@ export const INSTANCE_COMMANDS = [
|
||||
AddCacheTokensToAgentChatThreadFastInstanceCommand,
|
||||
AddLogoToApplicationFastInstanceCommand,
|
||||
AddDeletedAtToAgentChatThreadFastInstanceCommand,
|
||||
RemoveUserDefaultAvatarUrlFastInstanceCommand,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user