diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 404f91bb3e..814444f70e 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -863,7 +863,6 @@ type User { firstName: String! lastName: String! email: String! - defaultAvatarUrl: String isEmailVerified: Boolean! disabled: Boolean canImpersonate: Boolean! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index fb3692a3eb..748387e124 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -648,7 +648,6 @@ export interface User { firstName: Scalars['String'] lastName: Scalars['String'] email: Scalars['String'] - defaultAvatarUrl?: Scalars['String'] isEmailVerified: Scalars['Boolean'] disabled?: Scalars['Boolean'] canImpersonate: Scalars['Boolean'] @@ -3543,7 +3542,6 @@ export interface UserGenqlSelection{ firstName?: boolean | number lastName?: boolean | number email?: boolean | number - defaultAvatarUrl?: boolean | number isEmailVerified?: boolean | number disabled?: boolean | number canImpersonate?: boolean | number diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 16392b13a3..8de449535f 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -1830,9 +1830,6 @@ export default { "email": [ 1 ], - "defaultAvatarUrl": [ - 1 - ], "isEmailVerified": [ 6 ], diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index b76672d4bf..1f05f2f534 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -5482,7 +5482,6 @@ export type User = { createdAt: Scalars['DateTime']; currentUserWorkspace?: Maybe; currentWorkspace?: Maybe; - defaultAvatarUrl?: Maybe; deletedAt?: Maybe; deletedWorkspaceMembers?: Maybe>; disabled?: Maybe; @@ -7235,7 +7234,7 @@ export type UploadWorkspaceMemberProfilePictureMutationVariables = Exact<{ }>; -export type UploadWorkspaceMemberProfilePictureMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePicture: { __typename?: 'FileWithSignedUrl', url: string } }; +export type UploadWorkspaceMemberProfilePictureMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePicture: { __typename?: 'FileWithSignedUrl', id: string, url: string } }; export type UpdateUserEmailMutationVariables = Exact<{ newEmail: Scalars['String']; @@ -8068,7 +8067,7 @@ export const EnterpriseCheckoutSessionDocument = {"kind":"Document","definitions export const EnterprisePortalSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterprisePortalSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterprisePortalSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"returnUrlPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}}}]}]}}]} as unknown as DocumentNode; export const EnterpriseSubscriptionStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"licensee"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"isCancellationScheduled"}}]}}]}}]} as unknown as DocumentNode; export const UpdateLabPublicFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlagInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateLabPublicFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]} as unknown as DocumentNode; -export const UploadWorkspaceMemberProfilePictureDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceMemberProfilePicture"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceMemberProfilePicture"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; +export const UploadWorkspaceMemberProfilePictureDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceMemberProfilePicture"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceMemberProfilePicture"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; export const UpdateUserEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"newEmail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}}},{"kind":"Argument","name":{"kind":"Name","value":"verifyEmailRedirectPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}}}]}]}}]} as unknown as DocumentNode; export const UpdateWorkspaceMemberSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceMemberSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateWorkspaceMemberSettingsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateWorkspaceMemberSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; export const CreateOneRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"createRoleInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRoleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"createRoleInput"},"value":{"kind":"Variable","name":{"kind":"Name","value":"createRoleInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RoleFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RoleFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Role"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"isEditable"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToUsers"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToAgents"}},{"kind":"Field","name":{"kind":"Name","value":"canBeAssignedToApiKeys"}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePicture.ts b/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePicture.ts index 073498c459..5958e8c38c 100644 --- a/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePicture.ts +++ b/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePicture.ts @@ -3,6 +3,7 @@ import { gql } from '@apollo/client'; export const UPLOAD_WORKSPACE_MEMBER_PROFILE_PICTURE = gql` mutation UploadWorkspaceMemberProfilePicture($file: Upload!) { uploadWorkspaceMemberProfilePicture(file: $file) { + id url } } diff --git a/packages/twenty-front/src/modules/settings/workspace-member/components/WorkspaceMemberPictureUploader.tsx b/packages/twenty-front/src/modules/settings/workspace-member/components/WorkspaceMemberPictureUploader.tsx index a91d64c449..869a34037d 100644 --- a/packages/twenty-front/src/modules/settings/workspace-member/components/WorkspaceMemberPictureUploader.tsx +++ b/packages/twenty-front/src/modules/settings/workspace-member/components/WorkspaceMemberPictureUploader.tsx @@ -6,9 +6,12 @@ import { useCanEditProfileField } from '@/settings/profile/hooks/useCanEditProfi import { useUpdateWorkspaceMemberSettings } from '@/settings/profile/hooks/useUpdateWorkspaceMemberSettings'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ImageInput } from '@/ui/input/components/ImageInput'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useMutation } from '@apollo/client/react'; +import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { UploadWorkspaceMemberProfilePictureDocument } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -32,6 +35,9 @@ export const WorkspaceMemberPictureUploader = ({ useState(null); const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); + const setCurrentWorkspaceMember = useSetAtomState( + currentWorkspaceMemberState, + ); const [uploadPicture] = useMutation( UploadWorkspaceMemberProfilePictureDocument, @@ -66,19 +72,27 @@ export const WorkspaceMemberPictureUploader = ({ }, }); - const signedFile = data?.uploadWorkspaceMemberProfilePicture; - if (!isDefined(signedFile)) { + const uploadedFile = data?.uploadWorkspaceMemberProfilePicture; + if (!isDefined(uploadedFile)) { throw new Error('Avatar upload failed'); } - newAvatarUrl = signedFile.url; + newAvatarUrl = `${REACT_APP_SERVER_BASE_URL}/file/${FileFolder.CorePicture}/${uploadedFile.id}`; await updateWorkspaceMemberSettings({ workspaceMemberId, update: { avatarUrl: newAvatarUrl }, }); + const signedUrl = uploadedFile.url; + + if (isDefined(signedUrl) && isEditingSelf) { + setCurrentWorkspaceMember((previous) => + previous ? { ...previous, avatarUrl: signedUrl } : previous, + ); + } + if (isDefined(onAvatarUpdated)) { - onAvatarUpdated(newAvatarUrl); + onAvatarUpdated(signedUrl ?? newAvatarUrl); } setUploadController(null); @@ -104,7 +118,7 @@ export const WorkspaceMemberPictureUploader = ({ try { await updateWorkspaceMemberSettings({ workspaceMemberId, - update: { avatarUrl: '' }, + update: { avatarUrl: null }, }); if (isDefined(onAvatarUpdated)) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url.ts new file mode 100644 index 0000000000..a5a720b992 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url.ts @@ -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 { + await queryRunner.query( + 'ALTER TABLE "core"."user" DROP COLUMN "defaultAvatarUrl"', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'ALTER TABLE "core"."user" ADD "defaultAvatarUrl" character varying', + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-upgrade-version-command.module.ts index db90d5e394..7d68dd2094 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-upgrade-version-command.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777920000000-backfill-image-identifier-field-metadata-id.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777920000000-backfill-image-identifier-field-metadata-id.command.ts new file mode 100644 index 0000000000..89a42c492c --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-workspace-command-1777920000000-backfill-image-identifier-field-metadata-id.command.ts @@ -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 { + const isDryRun = options.dryRun ?? false; + + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + ]); + + const existingObject = + findFlatEntityByUniversalIdentifier({ + 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({ + 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}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index 0e734e6c4b..db50b39235 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -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, ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts index e6cad17142..af5f5c3777 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler.ts @@ -26,7 +26,10 @@ export class WorkspaceMemberQueryResultGetterHandler ); if (!isDefined(fileId)) { - return workspaceMember; + return { + ...workspaceMember, + avatarUrl: '', + }; } const signedUrl = this.fileUrlService.signFileByIdUrl({ diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts index 070333153e..eafbf98eab 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-statistics.service.ts @@ -153,7 +153,7 @@ export class AdminPanelStatisticsService { >(); for (const user of users) { - signedAvatarUrlByUserId.set(user.id, user.defaultAvatarUrl ?? null); + signedAvatarUrlByUserId.set(user.id, null); for (const userWorkspace of user.userWorkspaces) { const workspace = userWorkspace.workspace; @@ -169,7 +169,7 @@ export class AdminPanelStatisticsService { entry.fallbackAvatarUrlsByUserId.set( user.id, - userWorkspace.defaultAvatarUrl ?? user.defaultAvatarUrl ?? null, + userWorkspace.defaultAvatarUrl ?? null, ); contextsByWorkspaceId.set(workspace.id, entry); } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service.ts index 77a1a896a7..253f65a087 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/services/admin-panel-user-lookup.service.ts @@ -44,9 +44,7 @@ export class AdminPanelUserLookupService { .filter((workspaceUser) => isDefined(workspaceUser.user)) .map((workspaceUser) => [ workspaceUser.user.id, - workspaceUser.defaultAvatarUrl ?? - workspaceUser.user.defaultAvatarUrl ?? - null, + workspaceUser.defaultAvatarUrl ?? null, ]), ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts b/packages/twenty-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts index a8fb37f52d..6987b2f76b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts @@ -5,7 +5,6 @@ export const AUTH_CONTEXT_USER_SELECT_FIELDS = [ 'firstName', 'lastName', 'email', - 'defaultAvatarUrl', 'isEmailVerified', 'disabled', 'canImpersonate', diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.spec.ts index e3029f7350..fd116bb4eb 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.spec.ts @@ -85,7 +85,6 @@ describe('RefreshTokenService', () => { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', - defaultAvatarUrl: '', } as UserEntity; jest diff --git a/packages/twenty-server/src/engine/core-modules/file/file-url/file-url.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-url/file-url.service.ts index 11cb50ce26..6072a21954 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-url/file-url.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-url/file-url.service.ts @@ -66,4 +66,16 @@ export class FileUrlService { return `${serverUrl}/file/${fileFolder}/${fileId}?token=${token}`; } + + getLegacyWorkspaceMemberAvatarUrl({ + fileId, + fileFolder, + }: { + fileId: string; + fileFolder: FileFolder; + }): string { + const serverUrl = this.twentyConfigService.get('SERVER_URL'); + + return `${serverUrl}/file/${fileFolder}/${fileId}`; + } } diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts index 96f9dc8bf7..52c7297a0f 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts @@ -61,7 +61,6 @@ describe('TwoFactorAuthenticationResolver', () => { firstName: '', lastName: '', email: 'test@example.com', - defaultAvatarUrl: '', isEmailVerified: true, disabled: false, canImpersonate: false, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index 47688d54cd..72ce01aea9 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -248,7 +248,6 @@ describe('UserWorkspaceService', () => { email: 'test@example.com', firstName: 'John', lastName: 'Doe', - defaultAvatarUrl: 'avatar-url', locale: 'en', isEmailVerified: false, disabled: false, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index c9d9ab6415..4cc32042e9 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -162,7 +162,7 @@ export class UserWorkspaceService extends TypeOrmQueryService ({ firstName: entity.firstName, lastName: entity.lastName, email: entity.email, - defaultAvatarUrl: entity.defaultAvatarUrl, isEmailVerified: entity.isEmailVerified, disabled: entity.disabled, canImpersonate: entity.canImpersonate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap index 2a072bc3a4..1b3b1c980c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/__tests__/__snapshots__/all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec.ts.snap @@ -145,6 +145,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma "labelIdentifierFieldMetadataUniversalIdentifier", "standardOverrides", "isSearchable", + "imageIdentifierFieldMetadataUniversalIdentifier", ], "propertiesToStringify": [ "standardOverrides", diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts index 5ecc4b4fab..01f501d70e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts @@ -242,9 +242,10 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = { universalProperty: undefined, }, imageIdentifierFieldMetadataId: { - toCompare: false, + toCompare: true, toStringify: false, - universalProperty: undefined, + // @ts-expect-error remove once https://github.com/twentyhq/core-team-issues/issues/2172 has been resolved + universalProperty: 'imageIdentifierFieldMetadataUniversalIdentifier', }, targetTableName: { toCompare: false, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts index f396fa8b2c..3e82b3e7df 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/__tests__/flat-entity-update.type-test.ts @@ -49,6 +49,8 @@ type Assertions = [ | 'labelPlural' | 'labelIdentifierFieldMetadataId' | 'labelIdentifierFieldMetadataUniversalIdentifier' + | 'imageIdentifierFieldMetadataId' + | 'imageIdentifierFieldMetadataUniversalIdentifier' | 'isSearchable' > >, @@ -58,6 +60,7 @@ type Assertions = [ FlatEntityUpdate<'objectMetadata'>, { labelIdentifierFieldMetadataUniversalIdentifier?: never; + imageIdentifierFieldMetadataUniversalIdentifier?: never; } > >, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts index 680212639a..95e02c1011 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/create-standard-object-flat-metadata.util.ts @@ -61,6 +61,14 @@ export const createStandardObjectFlatMetadata = < labelIdentifierFieldMetadataName ].universalIdentifier; + const imageIdentifierFieldMetadataUniversalIdentifier = + imageIdentifierFieldMetadataName + ? // @ts-expect-error ignore + STANDARD_OBJECTS[nameSingular as keyof typeof STANDARD_OBJECTS].fields[ + imageIdentifierFieldMetadataName + ].universalIdentifier + : null; + return { universalIdentifier, applicationId: twentyStandardApplicationId, @@ -108,6 +116,6 @@ export const createStandardObjectFlatMetadata = < viewUniversalIdentifiers: [], indexMetadataUniversalIdentifiers: [], labelIdentifierFieldMetadataUniversalIdentifier, - imageIdentifierFieldMetadataUniversalIdentifier: null, + imageIdentifierFieldMetadataUniversalIdentifier, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts index a8f20cc309..1568ea4b6f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-entity-update.test-type.ts @@ -35,6 +35,7 @@ type Assertions = [ | 'labelSingular' | 'labelPlural' | 'labelIdentifierFieldMetadataUniversalIdentifier' + | 'imageIdentifierFieldMetadataUniversalIdentifier' | 'isSearchable' > >, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/update-object-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/update-object-action-handler.service.ts index ca3ce0daee..5602af10a8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/update-object-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/object/services/update-object-action-handler.service.ts @@ -56,8 +56,11 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc }); // TODO remove once https://github.com/twentyhq/core-team-issues/issues/2172 has been resolved - const { labelIdentifierFieldMetadataUniversalIdentifier, ...restUpdate } = - action.update; + const { + labelIdentifierFieldMetadataUniversalIdentifier, + imageIdentifierFieldMetadataUniversalIdentifier, + ...restUpdate + } = action.update; const transpiledUpdate: FlatEntityUpdate<'objectMetadata'> = { ...restUpdate, @@ -79,6 +82,22 @@ export class UpdateObjectActionHandlerService extends WorkspaceMigrationRunnerAc transpiledUpdate.labelIdentifierFieldMetadataId = flatFieldMetadata.id; } + if (isDefined(imageIdentifierFieldMetadataUniversalIdentifier)) { + const flatFieldMetadata = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: allFlatEntityMaps.flatFieldMetadataMaps, + universalIdentifier: imageIdentifierFieldMetadataUniversalIdentifier, + }); + + if (!isDefined(flatFieldMetadata)) { + throw new FlatEntityMapsException( + `Could not resolve imageIdentifierFieldMetadataUniversalIdentifier to imageIdentifierFieldMetadataId: no fieldMetadata found for universal identifier ${imageIdentifierFieldMetadataUniversalIdentifier}`, + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } + + transpiledUpdate.imageIdentifierFieldMetadataId = flatFieldMetadata.id; + } + return { type: 'update', metadataName: 'objectMetadata', diff --git a/packages/twenty-server/test/integration/graphql/suites/auth/sign-up/__snapshots__/successful-sign-up.integration-spec.ts.snap b/packages/twenty-server/test/integration/graphql/suites/auth/sign-up/__snapshots__/successful-sign-up.integration-spec.ts.snap index 4a801914d7..3d5883addb 100644 --- a/packages/twenty-server/test/integration/graphql/suites/auth/sign-up/__snapshots__/successful-sign-up.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/graphql/suites/auth/sign-up/__snapshots__/successful-sign-up.integration-spec.ts.snap @@ -7,7 +7,6 @@ exports[`Successful User Sign Up (integration) should sign up, delete and signup "createdAt": Any, "currentUserWorkspace": null, "currentWorkspace": null, - "defaultAvatarUrl": null, "deletedAt": null, "disabled": false, "email": "test-123@example.com", @@ -41,7 +40,6 @@ exports[`Successful User Sign Up (integration) should sign up, delete and signup "createdAt": Any, "currentUserWorkspace": null, "currentWorkspace": null, - "defaultAvatarUrl": null, "deletedAt": null, "disabled": false, "email": "test-123@example.com", diff --git a/packages/twenty-server/test/integration/graphql/utils/get-current-user.util.ts b/packages/twenty-server/test/integration/graphql/utils/get-current-user.util.ts index b95fca3dcb..01a255047c 100644 --- a/packages/twenty-server/test/integration/graphql/utils/get-current-user.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/get-current-user.util.ts @@ -24,7 +24,6 @@ export const getCurrentUser = async ({ email firstName lastName - defaultAvatarUrl isEmailVerified disabled canImpersonate diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index 13144da032..4bbac4eef7 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -1,4 +1,21 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Object metadata update should fail when imageIdentifier is not a uuid 1`] = ` +[ + { + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "http": { + "status": 400, + }, + "userFriendlyMessage": "An error occurred.", + "value": "not-a-uuid", + }, + "message": "Invalid UUID: 'not-a-uuid'", + "name": "ValidationError", + }, +] +`; exports[`Object metadata update should fail when labelIdentifier is not a TEXT or NAME field 1`] = ` [ diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts index 8f2f7183e5..e0166a4b2d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-update-one-object-metadata.integration-spec.ts @@ -43,7 +43,20 @@ const labelIdentifierFailingTestsUseCase: CreateOneObjectMetadataItemTestingCont }, ]; -const allTestsUseCases = [...labelIdentifierFailingTestsUseCase]; +const imageIdentifierFailingTestsUseCase: CreateOneObjectMetadataItemTestingContext = + [ + { + title: 'when imageIdentifier is not a uuid', + context: { + imageIdentifierFieldMetadataId: 'not-a-uuid', + }, + }, + ]; + +const allTestsUseCases = [ + ...labelIdentifierFailingTestsUseCase, + ...imageIdentifierFailingTestsUseCase, +]; describe('Object metadata update should fail', () => { let objectMetadataId: string;