From 187cf400aaaa13b33890f9c4d4135be03cdf7244 Mon Sep 17 00:00:00 2001 From: "twill-hq[bot]" <226015404+twill-hq[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:56:28 +0200 Subject: [PATCH] Fix author attachment field (#15065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Migrate Attachment Author to CreatedBy Field **Twill Task**: https://twill.ai/twentyhq/ENG/tasks/7 ## Summary This PR implements a migration to transition the `Attachment` object from using an `author` relation field to using the standard `createdBy` field, addressing issue https://github.com/twentyhq/core-team-issues/issues/1594. ## Changes - **Added migration command** (`1-8-migrate-attachment-author-to-created-by.command.ts`): - Migrates existing attachment data to use `createdBy` instead of `author` - Ensures data integrity during the transition to the standard field pattern - **Updated Attachment workspace entity**: - Added `createdBy` relation field to the `Attachment` standard object - Registered new field ID in `standard-field-ids.ts` constants - **Integrated migration into upgrade pipeline**: - Added migration module for version 1.8 - Registered in the main upgrade version command module This change aligns the `Attachment` object with Twenty's standard field conventions by using the built-in `createdBy` field instead of a custom `author` field. --- Fixes https://github.com/twentyhq/core-team-issues/issues/1594 --------- Co-authored-by: Twill Co-authored-by: Claude Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- CLAUDE.md | 6 +- .../blocks/components/FileBlock.tsx | 14 +- .../files/components/AttachmentRow.tsx | 2 +- .../files/hooks/useUploadAttachmentFile.tsx | 9 +- .../activities/files/types/Attachment.ts | 24 +- .../files/types/AttachmentFileCategory.ts | 9 + .../files/utils/__tests__/getFileType.test.ts | 16 +- .../activities/files/utils/getFileType.ts | 122 ++++----- ...findActivitiesOperationSignatureFactory.ts | 6 + .../internal/AgentChatFilePreview.tsx | 18 +- .../src/modules/file/components/FileIcon.tsx | 17 +- .../file/hooks/useFileCategoryColors.ts | 47 ++++ .../modules/file/utils/fileIconMappings.ts | 39 +-- .../__tests__/upgrade.command-runner.spec.ts | 5 + ...attachment-author-to-created-by.command.ts | 108 ++++++++ ...ttachment-type-to-file-category.command.ts | 83 ++++++ .../1-10-upgrade-version-command.module.ts | 23 ++ .../1-8/1-8-upgrade-version-command.module.ts | 6 +- .../upgrade-version-command.module.ts | 2 + .../upgrade.command.ts | 19 +- .../__tests__/worker.health.spec.ts | 1 + .../attachment-data-seeds.constant.ts | 238 +++++++++++++----- .../constants/standard-field-ids.ts | 2 + .../attachment.workspace-entity.ts | 80 +++++- ...space-member-delete-one.post-query.hook.ts | 12 - .../attachments.integration-spec.ts | 1 - 26 files changed, 686 insertions(+), 223 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/files/types/AttachmentFileCategory.ts create mode 100644 packages/twenty-front/src/modules/file/hooks/useFileCategoryColors.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts diff --git a/CLAUDE.md b/CLAUDE.md index e7a178b810..106a735a64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ npx nx storybook:build twenty-front # Build Storybook npx nx storybook:serve-and-test:static twenty-front # Run Storybook tests -When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials. +When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials. ``` ### Code Quality @@ -68,7 +68,7 @@ npx nx run twenty-server:database:migrate:prod # Run migrations npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts # Sync metadata -npx nx run twenty-server:command workspace:sync-metadata -f +npx nx run twenty-server:command workspace:sync-metadata ``` ### GraphQL @@ -149,4 +149,4 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li - `nx.json` - Nx workspace configuration with task definitions - `tsconfig.base.json` - Base TypeScript configuration - `package.json` - Root package with workspace definitions -- `.cursor/rules/` - Development guidelines and best practices \ No newline at end of file +- `.cursor/rules/` - Development guidelines and best practices diff --git a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx index a4363347c2..9c431b095e 100644 --- a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx @@ -4,11 +4,11 @@ import { isNonEmptyString } from '@sniptt/guards'; import { type ChangeEvent, useRef } from 'react'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory'; +import { getFileType } from '@/activities/files/utils/getFileType'; import { FileIcon } from '@/file/components/FileIcon'; import { isDefined } from 'twenty-shared/utils'; import { Button } from 'twenty-ui/input'; -import { type AttachmentType } from '../../files/types/Attachment'; -import { getFileType } from '../../files/utils/getFileType'; const StyledFileInput = styled.input` display: none; @@ -46,8 +46,8 @@ export const FileBlock = createReactBlockSpec( name: { default: '' as string, }, - fileType: { - default: 'Other' as AttachmentType, + fileCategory: { + default: 'OTHER' as AttachmentFileCategory, }, }, content: 'none', @@ -72,7 +72,7 @@ export const FileBlock = createReactBlockSpec( ...block.props, ...{ url: fileUrl, - fileType: getFileType(file.name), + fileCategory: getFileType(file.name), name: file.name, }, }, @@ -89,7 +89,9 @@ export const FileBlock = createReactBlockSpec( if (isNonEmptyString(block.props.url)) { return ( - + {block.props.name} diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index 20b2a04189..b6c1a92ee2 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -162,7 +162,7 @@ export const AttachmentRow = ({ > - + {isEditing ? ( { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const coreClient = useApolloCoreClient(); const [uploadFile] = useUploadFileMutation({ client: coreClient }); @@ -49,13 +45,10 @@ export const useUploadAttachmentFile = () => { }); const attachmentToCreate = { - authorId: currentWorkspaceMember?.id, name: file.name, fullPath: attachmentPath, - type: getFileType(file.name), + fileCategory: getFileType(file.name), [targetableObjectFieldIdName]: targetableObject.id, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), } as Partial; const createdAttachment = await createOneAttachment(attachmentToCreate); diff --git a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts index 20cb73984c..8cd9a1353a 100644 --- a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts +++ b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts @@ -1,21 +1,21 @@ +import { type AttachmentFileCategory } from './AttachmentFileCategory'; + +export type { AttachmentFileCategory }; + export type Attachment = { id: string; name: string; fullPath: string; - type: AttachmentType; + fileCategory: AttachmentFileCategory; companyId: string; personId: string; - authorId: string; + // Deprecated: Use createdBy instead + authorId?: string; + createdBy?: { + source: string; + workspaceMemberId: string | null; + name: string; + }; createdAt: string; __typename: string; }; - -export type AttachmentType = - | 'Archive' - | 'Audio' - | 'Image' - | 'Presentation' - | 'Spreadsheet' - | 'TextDocument' - | 'Video' - | 'Other'; diff --git a/packages/twenty-front/src/modules/activities/files/types/AttachmentFileCategory.ts b/packages/twenty-front/src/modules/activities/files/types/AttachmentFileCategory.ts new file mode 100644 index 0000000000..d5a74f8439 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/files/types/AttachmentFileCategory.ts @@ -0,0 +1,9 @@ +export type AttachmentFileCategory = + | 'ARCHIVE' + | 'AUDIO' + | 'IMAGE' + | 'PRESENTATION' + | 'SPREADSHEET' + | 'TEXT_DOCUMENT' + | 'VIDEO' + | 'OTHER'; diff --git a/packages/twenty-front/src/modules/activities/files/utils/__tests__/getFileType.test.ts b/packages/twenty-front/src/modules/activities/files/utils/__tests__/getFileType.test.ts index c60c0512e9..c66b9686a9 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/__tests__/getFileType.test.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/__tests__/getFileType.test.ts @@ -1,13 +1,13 @@ import { getFileType } from '../getFileType'; describe('getFileType', () => { - it('should return the correct file type for a given file name', () => { - expect(getFileType('test.doc')).toBe('TextDocument'); - expect(getFileType('test.xls')).toBe('Spreadsheet'); - expect(getFileType('test.ppt')).toBe('Presentation'); - expect(getFileType('test.png')).toBe('Image'); - expect(getFileType('test.mp4')).toBe('Video'); - expect(getFileType('test.mp3')).toBe('Audio'); - expect(getFileType('test.zip')).toBe('Archive'); + it('should return the correct file category for a given file name', () => { + expect(getFileType('test.doc')).toBe('TEXT_DOCUMENT'); + expect(getFileType('test.xls')).toBe('SPREADSHEET'); + expect(getFileType('test.ppt')).toBe('PRESENTATION'); + expect(getFileType('test.png')).toBe('IMAGE'); + expect(getFileType('test.mp4')).toBe('VIDEO'); + expect(getFileType('test.mp3')).toBe('AUDIO'); + expect(getFileType('test.zip')).toBe('ARCHIVE'); }); }); diff --git a/packages/twenty-front/src/modules/activities/files/utils/getFileType.ts b/packages/twenty-front/src/modules/activities/files/utils/getFileType.ts index 2a6b4dab91..198022e712 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/getFileType.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/getFileType.ts @@ -1,67 +1,69 @@ -import { type AttachmentType } from '@/activities/files/types/Attachment'; +import { type AttachmentFileCategory } from '@/activities/files/types/Attachment'; -const FileExtensionMapping: { [key: string]: AttachmentType } = { - doc: 'TextDocument', - docm: 'TextDocument', - docx: 'TextDocument', - dot: 'TextDocument', - dotx: 'TextDocument', - odt: 'TextDocument', - pdf: 'TextDocument', - txt: 'TextDocument', - rtf: 'TextDocument', - ps: 'TextDocument', - tex: 'TextDocument', - pages: 'TextDocument', - xls: 'Spreadsheet', - xlsb: 'Spreadsheet', - xlsm: 'Spreadsheet', - xlsx: 'Spreadsheet', - xltx: 'Spreadsheet', - csv: 'Spreadsheet', - tsv: 'Spreadsheet', - ods: 'Spreadsheet', - numbers: 'Spreadsheet', - ppt: 'Presentation', - pptx: 'Presentation', - potx: 'Presentation', - odp: 'Presentation', - html: 'Presentation', - key: 'Presentation', - kth: 'Presentation', - png: 'Image', - jpg: 'Image', - jpeg: 'Image', - svg: 'Image', - gif: 'Image', - webp: 'Image', - heif: 'Image', - tif: 'Image', - tiff: 'Image', - bmp: 'Image', - ico: 'Image', - mp4: 'Video', - avi: 'Video', - mov: 'Video', - wmv: 'Video', - mpg: 'Video', - mpeg: 'Video', - mp3: 'Audio', - wav: 'Audio', - ogg: 'Audio', - wma: 'Audio', - zip: 'Archive', - tar: 'Archive', - iso: 'Archive', - gz: 'Archive', - rar: 'Archive', - '7z': 'Archive', +const FileExtensionMapping: { + [key: string]: AttachmentFileCategory; +} = { + doc: 'TEXT_DOCUMENT', + docm: 'TEXT_DOCUMENT', + docx: 'TEXT_DOCUMENT', + dot: 'TEXT_DOCUMENT', + dotx: 'TEXT_DOCUMENT', + odt: 'TEXT_DOCUMENT', + pdf: 'TEXT_DOCUMENT', + txt: 'TEXT_DOCUMENT', + rtf: 'TEXT_DOCUMENT', + ps: 'TEXT_DOCUMENT', + tex: 'TEXT_DOCUMENT', + pages: 'TEXT_DOCUMENT', + xls: 'SPREADSHEET', + xlsb: 'SPREADSHEET', + xlsm: 'SPREADSHEET', + xlsx: 'SPREADSHEET', + xltx: 'SPREADSHEET', + csv: 'SPREADSHEET', + tsv: 'SPREADSHEET', + ods: 'SPREADSHEET', + numbers: 'SPREADSHEET', + ppt: 'PRESENTATION', + pptx: 'PRESENTATION', + potx: 'PRESENTATION', + odp: 'PRESENTATION', + html: 'PRESENTATION', + key: 'PRESENTATION', + kth: 'PRESENTATION', + png: 'IMAGE', + jpg: 'IMAGE', + jpeg: 'IMAGE', + svg: 'IMAGE', + gif: 'IMAGE', + webp: 'IMAGE', + heif: 'IMAGE', + tif: 'IMAGE', + tiff: 'IMAGE', + bmp: 'IMAGE', + ico: 'IMAGE', + mp4: 'VIDEO', + avi: 'VIDEO', + mov: 'VIDEO', + wmv: 'VIDEO', + mpg: 'VIDEO', + mpeg: 'VIDEO', + mp3: 'AUDIO', + wav: 'AUDIO', + ogg: 'AUDIO', + wma: 'AUDIO', + zip: 'ARCHIVE', + tar: 'ARCHIVE', + iso: 'ARCHIVE', + gz: 'ARCHIVE', + rar: 'ARCHIVE', + '7z': 'ARCHIVE', }; -export const getFileType = (fileName: string): AttachmentType => { +export const getFileType = (fileName: string): AttachmentFileCategory => { const fileExtension = fileName.split('.').at(-1); if (!fileExtension) { - return 'Other'; + return 'OTHER'; } - return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'Other'; + return FileExtensionMapping[fileExtension.toLowerCase()] ?? 'OTHER'; }; diff --git a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts index d57555d2f6..e645ca300d 100644 --- a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory.ts @@ -34,7 +34,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur name: true, __typename: true, }, + // Deprecated: Use createdBy instead authorId: true, + createdBy: { + source: true, + workspaceMemberId: true, + name: true, + }, assigneeId: true, assignee: { id: true, diff --git a/packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx b/packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx index 4a3ac7daa2..b87360d977 100644 --- a/packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx +++ b/packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx @@ -1,9 +1,11 @@ +import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory'; import { getFileType } from '@/activities/files/utils/getFileType'; -import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings'; +import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors'; +import { IconMapping } from '@/file/utils/fileIconMappings'; import { useTheme } from '@emotion/react'; import { type FileUIPart } from 'ai'; import { AvatarChip, Chip, ChipVariant } from 'twenty-ui/components'; -import { IconX } from 'twenty-ui/display'; +import { type IconComponent, IconX } from 'twenty-ui/display'; import { Loader } from 'twenty-ui/feedback'; export const AgentChatFilePreview = ({ @@ -16,11 +18,17 @@ export const AgentChatFilePreview = ({ isUploading?: boolean; }) => { const theme = useTheme(); - const iconColors = useFileTypeColors(); + const iconColors: Record = + useFileCategoryColors(); const fileName = file instanceof File ? file.name : (file.filename ?? 'Unknown file'); + const fileCategory: AttachmentFileCategory = getFileType(fileName); + + const FileCategoryIcon: IconComponent = IconMapping[fileCategory]; + const iconBackgroundColor: string = iconColors[fileCategory]; + return ( ) : ( ) } diff --git a/packages/twenty-front/src/modules/file/components/FileIcon.tsx b/packages/twenty-front/src/modules/file/components/FileIcon.tsx index 542e272f44..0f0d91aecb 100644 --- a/packages/twenty-front/src/modules/file/components/FileIcon.tsx +++ b/packages/twenty-front/src/modules/file/components/FileIcon.tsx @@ -1,5 +1,6 @@ -import { type AttachmentType } from '@/activities/files/types/Attachment'; -import { IconMapping, useFileTypeColors } from '@/file/utils/fileIconMappings'; +import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory'; +import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors'; +import { IconMapping } from '@/file/utils/fileIconMappings'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -14,14 +15,18 @@ const StyledIconContainer = styled.div<{ background: string }>` padding: ${({ theme }) => theme.spacing(1.25)}; `; -export const FileIcon = ({ fileType }: { fileType: AttachmentType }) => { +export const FileIcon = ({ + fileCategory, +}: { + fileCategory: AttachmentFileCategory; +}) => { const theme = useTheme(); - const iconColors = useFileTypeColors(); + const iconColors = useFileCategoryColors(); - const Icon = IconMapping[fileType]; + const Icon = IconMapping[fileCategory]; return ( - + {Icon && } ); diff --git a/packages/twenty-front/src/modules/file/hooks/useFileCategoryColors.ts b/packages/twenty-front/src/modules/file/hooks/useFileCategoryColors.ts new file mode 100644 index 0000000000..a8e8b1dad9 --- /dev/null +++ b/packages/twenty-front/src/modules/file/hooks/useFileCategoryColors.ts @@ -0,0 +1,47 @@ +import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useTheme } from '@emotion/react'; +import { isDefined } from 'twenty-shared/utils'; +import { type ThemeColor } from 'twenty-ui/theme'; + +export const useFileCategoryColors = (): Record< + AttachmentFileCategory, + string +> => { + const theme = useTheme(); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.Attachment, + }); + + const fileCategoryField = objectMetadataItem.fields.find( + (field) => field.name === 'fileCategory', + ); + + const colorMap: Record = { + ARCHIVE: theme.color.gray, + AUDIO: theme.color.pink, + IMAGE: theme.color.yellow, + PRESENTATION: theme.color.orange, + SPREADSHEET: theme.color.turquoise, + TEXT_DOCUMENT: theme.color.blue, + VIDEO: theme.color.purple, + OTHER: theme.color.gray, + }; + + if (isDefined(fileCategoryField?.options)) { + fileCategoryField.options.forEach((option) => { + const category = option.value as AttachmentFileCategory; + const color = option.color as ThemeColor; + if ( + isDefined(category) && + isDefined(color) && + isDefined(theme.color[color]) + ) { + colorMap[category] = theme.color[color]; + } + }); + } + + return colorMap; +}; diff --git a/packages/twenty-front/src/modules/file/utils/fileIconMappings.ts b/packages/twenty-front/src/modules/file/utils/fileIconMappings.ts index 7dbf604614..9adf8e9d76 100644 --- a/packages/twenty-front/src/modules/file/utils/fileIconMappings.ts +++ b/packages/twenty-front/src/modules/file/utils/fileIconMappings.ts @@ -1,5 +1,4 @@ -import { type AttachmentType } from '@/activities/files/types/Attachment'; -import { useTheme } from '@emotion/react'; +import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory'; import { type IconComponent, IconFile, @@ -12,29 +11,15 @@ import { IconVideo, } from 'twenty-ui/display'; -export const IconMapping: { [key in AttachmentType]: IconComponent } = { - Archive: IconFileZip, - Audio: IconHeadphones, - Image: IconPhoto, - Presentation: IconPresentation, - Spreadsheet: IconTable, - TextDocument: IconFileText, - Video: IconVideo, - Other: IconFile, -}; - -const getIconColors = (theme: any): { [key in AttachmentType]: string } => ({ - Archive: theme.color.gray, - Audio: theme.color.pink, - Image: theme.color.yellow, - Presentation: theme.color.orange, - Spreadsheet: theme.color.turquoise, - TextDocument: theme.color.blue, - Video: theme.color.purple, - Other: theme.color.gray, -}); - -export const useFileTypeColors = () => { - const theme = useTheme(); - return getIconColors(theme); +export const IconMapping: { + [key in AttachmentFileCategory]: IconComponent; +} = { + ARCHIVE: IconFileZip, + AUDIO: IconHeadphones, + IMAGE: IconPhoto, + PRESENTATION: IconPresentation, + SPREADSHEET: IconTable, + TEXT_DOCUMENT: IconFileText, + VIDEO: IconVideo, + OTHER: IconFile, }; diff --git a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts b/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts index 85dd318b73..30f671c3f2 100644 --- a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts +++ b/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts @@ -160,6 +160,11 @@ describe('UpgradeCommandRunner', () => { }); upgradeCommandRunner = module.get(commandRunner); + + jest.spyOn(upgradeCommandRunner['logger'], 'log').mockImplementation(); + jest.spyOn(upgradeCommandRunner['logger'], 'error').mockImplementation(); + jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation(); + runBeforeSyncMetadataSpy = jest.spyOn( upgradeCommandRunner, 'runBeforeSyncMetadata', diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts new file mode 100644 index 0000000000..9db3d6b362 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts @@ -0,0 +1,108 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { IsNull, Not, Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; +import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@Command({ + name: 'upgrade:1-10:migrate-attachment-author-to-created-by', + description: + 'Migrate attachment author field data to createdBy composite field', +}) +export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(Workspace) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + workspaceId, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Migrating attachment author to createdBy for workspace ${workspaceId}`, + ); + + const attachmentRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'attachment', + ); + + const workspaceMemberRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'workspaceMember', + ); + + const attachments = await attachmentRepository.find({ + where: { + authorId: Not(IsNull()), + createdBy: { + workspaceMemberId: IsNull(), + }, + }, + select: ['id', 'authorId'], + }); + + this.logger.log( + `Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`, + ); + + let migratedCount = 0; + + for (const attachment of attachments) { + const { id, authorId } = attachment; + + if (!isDefined(authorId)) { + continue; + } + + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { id: authorId }, + }); + + if (!isDefined(workspaceMember)) { + this.logger.warn( + `Workspace member ${authorId} not found for attachment ${id}, skipping`, + ); + continue; + } + + const firstName = workspaceMember.name?.firstName || ''; + const lastName = workspaceMember.name?.lastName || ''; + const displayName = + firstName || lastName ? `${firstName} ${lastName}`.trim() : 'Unknown'; + + await attachmentRepository.update( + { id }, + { + createdBy: { + source: FieldActorSource.MANUAL, + workspaceMemberId: workspaceMember.id, + name: displayName, + context: {}, + }, + }, + ); + + migratedCount++; + } + + this.logger.log( + `Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts new file mode 100644 index 0000000000..3f714866f5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts @@ -0,0 +1,83 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + type RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; + +const TYPE_TO_FILE_CATEGORY_MAPPING: Record = { + Archive: 'ARCHIVE', + Audio: 'AUDIO', + Image: 'IMAGE', + Presentation: 'PRESENTATION', + Spreadsheet: 'SPREADSHEET', + TextDocument: 'TEXT_DOCUMENT', + Video: 'VIDEO', + Other: 'OTHER', +}; + +@Command({ + name: 'upgrade:1-10:migrate-attachment-type-to-file-category', + description: + 'Migrate attachment type field data to fileCategory SELECT field', +}) +export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(Workspace) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + override async runOnWorkspace({ + workspaceId, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Migrating attachment type to fileCategory for workspace ${workspaceId}`, + ); + + const attachmentRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'attachment', + ); + + const attachments = await attachmentRepository.find({ + select: ['id', 'type'], + }); + + this.logger.log( + `Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`, + ); + + let migratedCount = 0; + + for (const attachment of attachments) { + const { id, type } = attachment; + + const fileCategory = + TYPE_TO_FILE_CATEGORY_MAPPING[type] || + TYPE_TO_FILE_CATEGORY_MAPPING.Other; + + await attachmentRepository.update( + { id }, + { + fileCategory, + }, + ); + + migratedCount++; + } + + this.logger.log( + `Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts new file mode 100644 index 0000000000..067b39e118 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command'; +import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace]), + WorkspaceSchemaManagerModule, + ], + providers: [ + MigrateAttachmentAuthorToCreatedByCommand, + MigrateAttachmentTypeToFileCategoryCommand, + ], + exports: [ + MigrateAttachmentAuthorToCreatedByCommand, + MigrateAttachmentTypeToFileCategoryCommand, + ], +}) +export class V1_10_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module.ts index 52b3e78cc4..10cd5524a0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command'; +import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command'; import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command'; import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value'; import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command'; @@ -10,12 +11,11 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; +import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; -import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command'; -import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; -import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; @Module({ imports: [ diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index d42fbaeaa2..62967c8181 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -10,6 +10,7 @@ import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade- import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module'; import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module'; import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module'; +import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module'; import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; @@ -26,6 +27,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp V1_6_UpgradeVersionCommandModule, V1_7_UpgradeVersionCommandModule, V1_8_UpgradeVersionCommandModule, + V1_10_UpgradeVersionCommandModule, WorkspaceSyncMetadataModule, ], providers: [UpgradeCommand], diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 8d48e54c88..231c8f79e5 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -19,6 +19,8 @@ import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upg import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command'; import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command'; import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command'; +import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command'; +import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command'; import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command'; import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command'; import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command'; @@ -30,6 +32,7 @@ import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command'; import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command'; import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command'; +import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command'; import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command'; import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value'; import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command'; @@ -37,7 +40,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; -import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command'; @Command({ name: 'upgrade', @@ -90,11 +92,15 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand, // 1.8 Commands + protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand, protected readonly migrateWorkflowStepFilterOperandValueCommand: MigrateWorkflowStepFilterOperandValueCommand, protected readonly deduplicateUniqueFieldsCommand: DeduplicateUniqueFieldsCommand, protected readonly regeneratePersonSearchVectorWithPhonesCommand: RegeneratePersonSearchVectorWithPhonesCommand, protected readonly migrateChannelSyncStagesCommand: MigrateChannelSyncStagesCommand, - protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand, + + // 1.10 Commands + protected readonly migrateAttachmentAuthorToCreatedByCommand: MigrateAttachmentAuthorToCreatedByCommand, + protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand, ) { super( workspaceRepository, @@ -199,6 +205,14 @@ export class UpgradeCommand extends UpgradeCommandRunner { afterSyncMetadata: [], }; + const commands_1100: VersionCommands = { + beforeSyncMetadata: [], + afterSyncMetadata: [ + this.migrateAttachmentAuthorToCreatedByCommand, + this.migrateAttachmentTypeToFileCategoryCommand, + ], + }; + this.allCommands = { '0.53.0': commands_053, '0.54.0': commands_054, @@ -213,6 +227,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { '1.6.0': commands_160, '1.7.0': commands_170, '1.8.0': commands_180, + '1.10.0': commands_1100, }; } diff --git a/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/worker.health.spec.ts b/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/worker.health.spec.ts index fc742423f4..ffd8c54683 100644 --- a/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/worker.health.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/worker.health.spec.ts @@ -35,6 +35,7 @@ describe('WorkerHealthIndicator', () => { const mockRedisService = { getClient: () => mockRedis, + getQueueClient: () => mockRedis, } as unknown as RedisClientService; healthIndicatorService = { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts index 5ae6002e08..8d3bb5a0dd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/attachment-data-seeds.constant.ts @@ -1,3 +1,4 @@ +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { COMPANY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/company-data-seeds.constant'; import { NOTE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/note-data-seeds.constant'; import { OPPORTUNITY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/opportunity-data-seeds.constant'; @@ -10,10 +11,12 @@ type AttachmentDataSeed = { name: string; fullPath: string; type: string; + fileCategory?: string; + // Deprecated: Use createdBy instead authorId: string | null; - // createdBySource: string; - // createdByWorkspaceMemberId: string; - // createdByName: string; + createdBySource: string; + createdByWorkspaceMemberId: string; + createdByName: string; personId: string | null; companyId: string | null; noteId: string | null; @@ -26,10 +29,11 @@ export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [ 'name', 'fullPath', 'type', - 'authorId', - // 'createdBySource', - // 'createdByWorkspaceMemberId', - // 'createdByName', + 'fileCategory', + 'authorId', // Deprecated: kept for backward compatibility during migration + 'createdBySource', + 'createdByWorkspaceMemberId', + 'createdByName', 'personId', 'companyId', 'noteId', @@ -52,69 +56,169 @@ const GENERATE_ATTACHMENT_IDS = (): Record => { export const ATTACHMENT_DATA_SEED_IDS = GENERATE_ATTACHMENT_IDS(); -// Pool of 5 reusable file templates -const FILE_TEMPLATES = [ - { - name: 'Contract Agreement.pdf', - fullPath: 'attachment/sample-contract.pdf', - type: 'TextDocument', - }, - { - name: 'Budget 2024.xlsx', - fullPath: 'attachment/budget-2024.xlsx', - type: 'Spreadsheet', - }, - { - name: 'Product Presentation.pptx', - fullPath: 'attachment/presentation.pptx', - type: 'Presentation', - }, - { - name: 'Screenshot.png', - fullPath: 'attachment/screenshot.png', - type: 'Image', - }, - { - name: 'Archive.zip', - fullPath: 'attachment/archive.zip', - type: 'Archive', - }, +// Pool of 5 reusable file paths for attachments +const FILE_TEMPLATE_PATHS = [ + 'attachment/sample-contract.pdf', + 'attachment/budget-2024.xlsx', + 'attachment/presentation.pptx', + 'attachment/screenshot.png', + 'attachment/archive.zip', ]; // Additional name variations for more realistic variety const FILE_NAME_VARIATIONS = [ // Documents - { name: 'Service Agreement.pdf', type: 'TextDocument', pathIndex: 0 }, - { name: 'NDA Document.pdf', type: 'TextDocument', pathIndex: 0 }, - { name: 'Project Proposal.pdf', type: 'TextDocument', pathIndex: 0 }, - { name: 'Invoice Q1 2024.pdf', type: 'TextDocument', pathIndex: 0 }, - { name: 'Meeting Notes.pdf', type: 'TextDocument', pathIndex: 0 }, - { name: 'Report Final.pdf', type: 'TextDocument', pathIndex: 0 }, - { name: 'Contract Signed.pdf', type: 'TextDocument', pathIndex: 0 }, + { + name: 'Service Agreement.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, + { + name: 'NDA Document.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, + { + name: 'Project Proposal.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, + { + name: 'Invoice Q1 2024.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, + { + name: 'Meeting Notes.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, + { + name: 'Report Final.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, + { + name: 'Contract Signed.pdf', + type: 'TextDocument', + fileCategory: 'TEXT_DOCUMENT', + pathIndex: 0, + }, // Spreadsheets - { name: 'Financial Forecast.xlsx', type: 'Spreadsheet', pathIndex: 1 }, - { name: 'Sales Report Q4.xlsx', type: 'Spreadsheet', pathIndex: 1 }, - { name: 'Team Roster.xlsx', type: 'Spreadsheet', pathIndex: 1 }, - { name: 'Expense Report.xlsx', type: 'Spreadsheet', pathIndex: 1 }, - { name: 'Inventory List.xlsx', type: 'Spreadsheet', pathIndex: 1 }, - { name: 'Data Export.csv', type: 'Spreadsheet', pathIndex: 1 }, + { + name: 'Financial Forecast.xlsx', + type: 'Spreadsheet', + fileCategory: 'SPREADSHEET', + pathIndex: 1, + }, + { + name: 'Sales Report Q4.xlsx', + type: 'Spreadsheet', + fileCategory: 'SPREADSHEET', + pathIndex: 1, + }, + { + name: 'Team Roster.xlsx', + type: 'Spreadsheet', + fileCategory: 'SPREADSHEET', + pathIndex: 1, + }, + { + name: 'Expense Report.xlsx', + type: 'Spreadsheet', + fileCategory: 'SPREADSHEET', + pathIndex: 1, + }, + { + name: 'Inventory List.xlsx', + type: 'Spreadsheet', + fileCategory: 'SPREADSHEET', + pathIndex: 1, + }, + { + name: 'Data Export.csv', + type: 'Spreadsheet', + fileCategory: 'SPREADSHEET', + pathIndex: 1, + }, // Presentations - { name: 'Pitch Deck.pptx', type: 'Presentation', pathIndex: 2 }, - { name: 'Q4 Results.pptx', type: 'Presentation', pathIndex: 2 }, - { name: 'Roadmap 2024.pptx', type: 'Presentation', pathIndex: 2 }, - { name: 'Company Overview.pptx', type: 'Presentation', pathIndex: 2 }, - { name: 'Training Materials.pptx', type: 'Presentation', pathIndex: 2 }, + { + name: 'Pitch Deck.pptx', + type: 'Presentation', + fileCategory: 'PRESENTATION', + pathIndex: 2, + }, + { + name: 'Q4 Results.pptx', + type: 'Presentation', + fileCategory: 'PRESENTATION', + pathIndex: 2, + }, + { + name: 'Roadmap 2024.pptx', + type: 'Presentation', + fileCategory: 'PRESENTATION', + pathIndex: 2, + }, + { + name: 'Company Overview.pptx', + type: 'Presentation', + fileCategory: 'PRESENTATION', + pathIndex: 2, + }, + { + name: 'Training Materials.pptx', + type: 'Presentation', + fileCategory: 'PRESENTATION', + pathIndex: 2, + }, // Images - { name: 'Company Logo.png', type: 'Image', pathIndex: 3 }, - { name: 'Product Photo.jpg', type: 'Image', pathIndex: 3 }, - { name: 'Diagram.png', type: 'Image', pathIndex: 3 }, - { name: 'Wireframe.png', type: 'Image', pathIndex: 3 }, - { name: 'Mockup Design.png', type: 'Image', pathIndex: 3 }, - { name: 'Headshot.jpg', type: 'Image', pathIndex: 3 }, + { + name: 'Company Logo.png', + type: 'Image', + fileCategory: 'IMAGE', + pathIndex: 3, + }, + { + name: 'Product Photo.jpg', + type: 'Image', + fileCategory: 'IMAGE', + pathIndex: 3, + }, + { name: 'Diagram.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 }, + { name: 'Wireframe.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 }, + { + name: 'Mockup Design.png', + type: 'Image', + fileCategory: 'IMAGE', + pathIndex: 3, + }, + { name: 'Headshot.jpg', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 }, // Archives - { name: 'Project Files.zip', type: 'Archive', pathIndex: 4 }, - { name: 'Backup Data.zip', type: 'Archive', pathIndex: 4 }, - { name: 'Source Code.zip', type: 'Archive', pathIndex: 4 }, + { + name: 'Project Files.zip', + type: 'Archive', + fileCategory: 'ARCHIVE', + pathIndex: 4, + }, + { + name: 'Backup Data.zip', + type: 'Archive', + fileCategory: 'ARCHIVE', + pathIndex: 4, + }, + { + name: 'Source Code.zip', + type: 'Archive', + fileCategory: 'ARCHIVE', + pathIndex: 4, + }, ]; const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => { @@ -133,7 +237,7 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => { // Cycle through file name variations const NAME_VARIATION_INDEX = INDEX % FILE_NAME_VARIATIONS.length; const NAME_VARIATION = FILE_NAME_VARIATIONS[NAME_VARIATION_INDEX]; - const FILE_TEMPLATE = FILE_TEMPLATES[NAME_VARIATION.pathIndex]; + const FILE_PATH = FILE_TEMPLATE_PATHS[NAME_VARIATION.pathIndex]; // Determine which entity this attachment belongs to // Distribution: ~30% person, ~30% company, ~20% note, ~15% task, ~5% opportunity @@ -170,12 +274,14 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => { ATTACHMENT_SEEDS.push({ id: ATTACHMENT_DATA_SEED_IDS[`ID_${INDEX}`], name: NAME_VARIATION.name, - fullPath: FILE_TEMPLATE.fullPath, + fullPath: FILE_PATH, type: NAME_VARIATION.type, + fileCategory: NAME_VARIATION.fileCategory, + // Deprecated: Use createdBy fields instead authorId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - // createdBySource: 'MANUAL', - //createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, - //createdByName: 'Tim A', + createdBySource: FieldActorSource.MANUAL, + createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM, + createdByName: 'Tim A', personId, companyId, noteId, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 20fda850d0..e405b0ff0a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -33,6 +33,8 @@ export const ATTACHMENT_STANDARD_FIELD_IDS = { name: '20202020-87a5-48f8-bbf7-ade388825a57', fullPath: '20202020-0d19-453d-8e8d-fbcda8ca3747', type: '20202020-a417-49b8-a40b-f6a7874caa0d', + fileCategory: '20202020-8c3f-4d9e-9a1b-2e5f7a8c9d0e', + createdBy: '395be3bd-a5c9-463d-aafe-9bc3bbec3f15', author: '20202020-6501-4ac5-a4ef-b2f8522ef6cd', activity: '20202020-b569-481b-a13f-9b94e47e54fe', task: '20202020-51e5-4621-9cf8-215487951c4b', diff --git a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts index 262c42587f..4a4777572c 100644 --- a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts +++ b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts @@ -6,12 +6,14 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; +import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; @@ -57,20 +59,92 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity { }) fullPath: string; + // Deprecated: Use fileCategory instead @WorkspaceField({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.type, type: FieldMetadataType.TEXT, - label: msg`Type`, - description: msg`Attachment type`, + label: msg`Type (deprecated)`, + description: msg`Attachment type (deprecated - use fileCategory)`, icon: 'IconList', }) type: string; + @WorkspaceField({ + standardId: ATTACHMENT_STANDARD_FIELD_IDS.fileCategory, + type: FieldMetadataType.SELECT, + label: msg`File category`, + description: msg`Attachment file category`, + icon: 'IconList', + options: [ + { + value: 'ARCHIVE', + label: 'Archive', + position: 0, + color: 'gray', + }, + { + value: 'AUDIO', + label: 'Audio', + position: 1, + color: 'pink', + }, + { + value: 'IMAGE', + label: 'Image', + position: 2, + color: 'yellow', + }, + { + value: 'PRESENTATION', + label: 'Presentation', + position: 3, + color: 'orange', + }, + { + value: 'SPREADSHEET', + label: 'Spreadsheet', + position: 4, + color: 'turquoise', + }, + { + value: 'TEXT_DOCUMENT', + label: 'Text Document', + position: 5, + color: 'blue', + }, + { + value: 'VIDEO', + label: 'Video', + position: 6, + color: 'purple', + }, + { + value: 'OTHER', + label: 'Other', + position: 7, + color: 'gray', + }, + ], + defaultValue: "'OTHER'", + }) + fileCategory: string; + + @WorkspaceField({ + standardId: ATTACHMENT_STANDARD_FIELD_IDS.createdBy, + type: FieldMetadataType.ACTOR, + label: msg`Created by`, + icon: 'IconCreativeCommonsSa', + description: msg`The creator of the record`, + }) + @WorkspaceIsFieldUIReadOnly() + createdBy: ActorMetadata; + + // Deprecated: Use createdBy composite field instead @WorkspaceRelation({ standardId: ATTACHMENT_STANDARD_FIELD_IDS.author, type: RelationType.MANY_TO_ONE, label: msg`Author`, - description: msg`Attachment author`, + description: msg`Attachment author (deprecated - use createdBy)`, icon: 'IconCircleUser', inverseSideTarget: () => WorkspaceMemberWorkspaceEntity, inverseSideFieldKey: 'authoredAttachments', diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts index e4ee1768c8..a4733818ca 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts @@ -15,7 +15,6 @@ import { PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -59,17 +58,6 @@ export class WorkspaceMemberDeleteOnePostQueryHook }, ); - const attachmentRepository = - await this.twentyORMManager.getRepository( - 'attachment', - ); - - const authorId = targettedWorkspaceMemberId; - - await attachmentRepository.delete({ - authorId, - }); - const workspaceMemberRepository = await this.twentyORMManager.getRepository( 'workspaceMember', diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts index 5c173a1385..137afc8e70 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts @@ -59,7 +59,6 @@ describe('attachmentsResolver (e2e)', () => { expect(attachments).toHaveProperty('createdAt'); expect(attachments).toHaveProperty('updatedAt'); expect(attachments).toHaveProperty('deletedAt'); - expect(attachments).toHaveProperty('authorId'); expect(attachments).toHaveProperty('taskId'); expect(attachments).toHaveProperty('noteId'); expect(attachments).toHaveProperty('personId');