diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 97ebd9b4d6..aefa4a3c9e 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -174,6 +174,7 @@ export type AgentMessagePart = { errorDetails?: Maybe; errorMessage?: Maybe; fileFilename?: Maybe; + fileId?: Maybe; fileMediaType?: Maybe; fileUrl?: Maybe; id: Scalars['UUID']; @@ -1624,19 +1625,16 @@ export enum FeatureFlagKey { IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED', - IS_CORE_PICTURE_MIGRATED = 'IS_CORE_PICTURE_MIGRATED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED = 'IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED', IS_DRAFT_EMAIL_ENABLED = 'IS_DRAFT_EMAIL_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', - IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED', IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED = 'IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED', IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED', IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED', - IS_OTHER_FILE_MIGRATED = 'IS_OTHER_FILE_MIGRATED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED', IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED', @@ -2352,8 +2350,6 @@ export type Mutation = { createCoreViewSort: CoreViewSort; createDatabaseConfigVariable: Scalars['Boolean']; createEmailingDomain: EmailingDomain; - /** @deprecated Use specific file service instead */ - createFile: File; createFrontComponent: FrontComponent; createManyCoreViewFieldGroups: Array; createManyCoreViewFields: Array; @@ -2390,8 +2386,6 @@ export type Mutation = { deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; deleteEmailingDomain: Scalars['Boolean']; - /** @deprecated */ - deleteFile: File; deleteFrontComponent: FrontComponent; deleteJobs: DeleteJobsResponse; deleteNavigationMenuItem: NavigationMenuItem; @@ -2496,17 +2490,13 @@ export type Mutation = { updateWorkspace: Workspace; updateWorkspaceFeatureFlag: Scalars['Boolean']; updateWorkspaceMemberRole: WorkspaceMember; + uploadAIChatFile: FileWithSignedUrl; uploadApplicationFile: File; - /** @deprecated Use uploadFilesFieldFile instead */ - uploadFile: SignedFile; uploadFilesFieldFile: FileWithSignedUrl; uploadFilesFieldFileByUniversalIdentifier: FileWithSignedUrl; - uploadImage: SignedFile; uploadWorkflowFile: FileWithSignedUrl; uploadWorkspaceLogo: FileWithSignedUrl; - uploadWorkspaceLogoLegacy: SignedFile; uploadWorkspaceMemberProfilePicture: FileWithSignedUrl; - uploadWorkspaceMemberProfilePictureLegacy: SignedFile; upsertFieldPermissions: Array; upsertFieldsWidget: CoreView; upsertObjectPermissions: Array; @@ -2642,11 +2632,6 @@ export type MutationCreateEmailingDomainArgs = { }; -export type MutationCreateFileArgs = { - file: Scalars['Upload']; -}; - - export type MutationCreateFrontComponentArgs = { input: CreateFrontComponentInput; }; @@ -2825,11 +2810,6 @@ export type MutationDeleteEmailingDomainArgs = { }; -export type MutationDeleteFileArgs = { - fileId: Scalars['UUID']; -}; - - export type MutationDeleteFrontComponentArgs = { id: Scalars['UUID']; }; @@ -3344,6 +3324,11 @@ export type MutationUpdateWorkspaceMemberRoleArgs = { }; +export type MutationUploadAiChatFileArgs = { + file: Scalars['Upload']; +}; + + export type MutationUploadApplicationFileArgs = { applicationUniversalIdentifier: Scalars['String']; file: Scalars['Upload']; @@ -3352,12 +3337,6 @@ export type MutationUploadApplicationFileArgs = { }; -export type MutationUploadFileArgs = { - file: Scalars['Upload']; - fileFolder?: InputMaybe; -}; - - export type MutationUploadFilesFieldFileArgs = { fieldMetadataId: Scalars['String']; file: Scalars['Upload']; @@ -3370,12 +3349,6 @@ export type MutationUploadFilesFieldFileByUniversalIdentifierArgs = { }; -export type MutationUploadImageArgs = { - file: Scalars['Upload']; - fileFolder?: InputMaybe; -}; - - export type MutationUploadWorkflowFileArgs = { file: Scalars['Upload']; }; @@ -3386,21 +3359,11 @@ export type MutationUploadWorkspaceLogoArgs = { }; -export type MutationUploadWorkspaceLogoLegacyArgs = { - file: Scalars['Upload']; -}; - - export type MutationUploadWorkspaceMemberProfilePictureArgs = { file: Scalars['Upload']; }; -export type MutationUploadWorkspaceMemberProfilePictureLegacyArgs = { - file: Scalars['Upload']; -}; - - export type MutationUpsertFieldPermissionsArgs = { upsertFieldPermissionsInput: UpsertFieldPermissionsInput; }; @@ -4634,12 +4597,6 @@ export type SignUp = { workspace: WorkspaceUrlsAndId; }; -export type SignedFile = { - __typename?: 'SignedFile'; - path: Scalars['String']; - token: Scalars['String']; -}; - export type Skill = { __typename?: 'Skill'; applicationId?: Maybe; @@ -5725,6 +5682,13 @@ export type UpdateSkillMutationVariables = Exact<{ export type UpdateSkillMutation = { __typename?: 'Mutation', updateSkill: { __typename?: 'Skill', id: string, name: string, label: string, description?: string | null, icon?: string | null, content: string, isCustom: boolean, isActive: boolean, createdAt: string, updatedAt: string } }; +export type UploadAiChatFileMutationVariables = Exact<{ + file: Scalars['Upload']; +}>; + + +export type UploadAiChatFileMutation = { __typename?: 'Mutation', uploadAIChatFile: { __typename?: 'FileWithSignedUrl', id: string, path: string, size: number, createdAt: string, url: string } }; + export type FindManyAgentsQueryVariables = Exact<{ [key: string]: never; }>; @@ -5761,7 +5725,7 @@ export type GetChatMessagesQueryVariables = Exact<{ }>; -export type GetChatMessagesQuery = { __typename?: 'Query', chatMessages: Array<{ __typename?: 'AgentMessage', id: string, threadId: string, turnId: string, role: string, createdAt: string, parts: Array<{ __typename?: 'AgentMessagePart', id: string, messageId: string, orderIndex: number, type: string, textContent?: string | null, reasoningContent?: string | null, toolName?: string | null, toolCallId?: string | null, toolInput?: any | null, toolOutput?: any | null, state?: string | null, errorMessage?: string | null, errorDetails?: any | null, sourceUrlSourceId?: string | null, sourceUrlUrl?: string | null, sourceUrlTitle?: string | null, sourceDocumentSourceId?: string | null, sourceDocumentMediaType?: string | null, sourceDocumentTitle?: string | null, sourceDocumentFilename?: string | null, fileMediaType?: string | null, fileFilename?: string | null, fileUrl?: string | null, providerMetadata?: any | null, createdAt: string }> }> }; +export type GetChatMessagesQuery = { __typename?: 'Query', chatMessages: Array<{ __typename?: 'AgentMessage', id: string, threadId: string, turnId: string, role: string, createdAt: string, parts: Array<{ __typename?: 'AgentMessagePart', id: string, messageId: string, orderIndex: number, type: string, textContent?: string | null, reasoningContent?: string | null, toolName?: string | null, toolCallId?: string | null, toolInput?: any | null, toolOutput?: any | null, state?: string | null, errorMessage?: string | null, errorDetails?: any | null, sourceUrlSourceId?: string | null, sourceUrlUrl?: string | null, sourceUrlTitle?: string | null, sourceDocumentSourceId?: string | null, sourceDocumentMediaType?: string | null, sourceDocumentTitle?: string | null, sourceDocumentFilename?: string | null, fileMediaType?: string | null, fileFilename?: string | null, fileUrl?: string | null, fileId?: string | null, providerMetadata?: any | null, createdAt: string }> }> }; export type GetChatThreadsQueryVariables = Exact<{ paging?: InputMaybe; @@ -5815,22 +5779,6 @@ export type FindOneApplicationQueryVariables = Exact<{ export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, defaultRoleId?: string | null, settingsCustomTabFrontComponentId?: string | null, availablePackages: any, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, universalIdentifier: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, universalIdentifier: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, logicFunctions: Array<{ __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, applicationId?: string | null, createdAt: string, updatedAt: string }> } }; -export type UploadFileMutationVariables = Exact<{ - file: Scalars['Upload']; - fileFolder?: InputMaybe; -}>; - - -export type UploadFileMutation = { __typename?: 'Mutation', uploadFile: { __typename?: 'SignedFile', path: string, token: string } }; - -export type UploadImageMutationVariables = Exact<{ - file: Scalars['Upload']; - fileFolder?: InputMaybe; -}>; - - -export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: { __typename?: 'SignedFile', path: string, token: string } }; - export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string }; export type AuthTokenPairFragmentFragment = { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } }; @@ -6134,20 +6082,6 @@ export type FindOnePageLayoutQueryVariables = Exact<{ export type FindOnePageLayoutQuery = { __typename?: 'Query', getPageLayout?: { __typename?: 'PageLayout', id: string, name: string, objectMetadataId?: string | null, type: PageLayoutType, defaultTabToFocusOnMobileAndSidePanelId?: string | null, createdAt: string, updatedAt: string, tabs?: Array<{ __typename?: 'PageLayoutTab', id: string, applicationId: string, title: string, icon?: string | null, position: number, layoutMode?: PageLayoutTabLayoutMode | null, pageLayoutId: string, createdAt: string, updatedAt: string, widgets?: Array<{ __typename?: 'PageLayoutWidget', id: string, title: string, type: WidgetType, objectMetadataId?: string | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: string, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, position?: { __typename?: 'PageLayoutWidgetCanvasPosition', layoutMode: PageLayoutTabLayoutMode } | { __typename?: 'PageLayoutWidgetGridPosition', layoutMode: PageLayoutTabLayoutMode, row: number, column: number, rowSpan: number, columnSpan: number } | { __typename?: 'PageLayoutWidgetVerticalListPosition', layoutMode: PageLayoutTabLayoutMode, index: number } | null, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: string, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType, viewId?: string | null } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: string } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: string, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: string | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, splitMultiValueFields?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: string, aggregateFieldMetadataId: string, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, splitMultiValueFields?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }> | null }> | null } | null }; -export type CreateFileMutationVariables = Exact<{ - file: Scalars['Upload']; -}>; - - -export type CreateFileMutation = { __typename?: 'Mutation', createFile: { __typename?: 'File', id: string, path: string, size: number, createdAt: string } }; - -export type DeleteFileMutationVariables = Exact<{ - fileId: Scalars['UUID']; -}>; - - -export type DeleteFileMutation = { __typename?: 'Mutation', deleteFile: { __typename?: 'File', id: string, path: string, size: number, createdAt: string } }; - export type UploadFilesFieldFileMutationVariables = Exact<{ file: Scalars['Upload']; fieldMetadataId: Scalars['String']; @@ -6745,13 +6679,6 @@ export type UploadWorkspaceMemberProfilePictureMutationVariables = Exact<{ export type UploadWorkspaceMemberProfilePictureMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePicture: { __typename?: 'FileWithSignedUrl', url: string } }; -export type UploadWorkspaceMemberProfilePictureLegacyMutationVariables = Exact<{ - file: Scalars['Upload']; -}>; - - -export type UploadWorkspaceMemberProfilePictureLegacyMutation = { __typename?: 'Mutation', uploadWorkspaceMemberProfilePictureLegacy: { __typename?: 'SignedFile', path: string, token: string } }; - export type UpdateUserEmailMutationVariables = Exact<{ newEmail: Scalars['String']; verifyEmailRedirectPath?: InputMaybe; @@ -7311,13 +7238,6 @@ export type UploadWorkspaceLogoMutationVariables = Exact<{ export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: { __typename?: 'FileWithSignedUrl', url: string } }; -export type UploadWorkspaceLogoLegacyMutationVariables = Exact<{ - file: Scalars['Upload']; -}>; - - -export type UploadWorkspaceLogoLegacyMutation = { __typename?: 'Mutation', uploadWorkspaceLogoLegacy: { __typename?: 'SignedFile', path: string, token: string } }; - export type CheckCustomDomainValidRecordsMutationVariables = Exact<{ [key: string]: never; }>; @@ -8845,6 +8765,43 @@ export function useUpdateSkillMutation(baseOptions?: Apollo.MutationHookOptions< export type UpdateSkillMutationHookResult = ReturnType; export type UpdateSkillMutationResult = Apollo.MutationResult; export type UpdateSkillMutationOptions = Apollo.BaseMutationOptions; +export const UploadAiChatFileDocument = gql` + mutation uploadAIChatFile($file: Upload!) { + uploadAIChatFile(file: $file) { + id + path + size + createdAt + url + } +} + `; +export type UploadAiChatFileMutationFn = Apollo.MutationFunction; + +/** + * __useUploadAiChatFileMutation__ + * + * To run a mutation, you first call `useUploadAiChatFileMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUploadAiChatFileMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [uploadAiChatFileMutation, { data, loading, error }] = useUploadAiChatFileMutation({ + * variables: { + * file: // value for 'file' + * }, + * }); + */ +export function useUploadAiChatFileMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UploadAiChatFileDocument, options); + } +export type UploadAiChatFileMutationHookResult = ReturnType; +export type UploadAiChatFileMutationResult = Apollo.MutationResult; +export type UploadAiChatFileMutationOptions = Apollo.BaseMutationOptions; export const FindManyAgentsDocument = gql` query FindManyAgents { findManyAgents { @@ -9088,6 +9045,7 @@ export const GetChatMessagesDocument = gql` fileMediaType fileFilename fileUrl + fileId providerMetadata createdAt } @@ -9390,76 +9348,6 @@ export function useFindOneApplicationLazyQuery(baseOptions?: Apollo.LazyQueryHoo export type FindOneApplicationQueryHookResult = ReturnType; export type FindOneApplicationLazyQueryHookResult = ReturnType; export type FindOneApplicationQueryResult = Apollo.QueryResult; -export const UploadFileDocument = gql` - mutation uploadFile($file: Upload!, $fileFolder: FileFolder) { - uploadFile(file: $file, fileFolder: $fileFolder) { - path - token - } -} - `; -export type UploadFileMutationFn = Apollo.MutationFunction; - -/** - * __useUploadFileMutation__ - * - * To run a mutation, you first call `useUploadFileMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUploadFileMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [uploadFileMutation, { data, loading, error }] = useUploadFileMutation({ - * variables: { - * file: // value for 'file' - * fileFolder: // value for 'fileFolder' - * }, - * }); - */ -export function useUploadFileMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UploadFileDocument, options); - } -export type UploadFileMutationHookResult = ReturnType; -export type UploadFileMutationResult = Apollo.MutationResult; -export type UploadFileMutationOptions = Apollo.BaseMutationOptions; -export const UploadImageDocument = gql` - mutation uploadImage($file: Upload!, $fileFolder: FileFolder) { - uploadImage(file: $file, fileFolder: $fileFolder) { - path - token - } -} - `; -export type UploadImageMutationFn = Apollo.MutationFunction; - -/** - * __useUploadImageMutation__ - * - * To run a mutation, you first call `useUploadImageMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUploadImageMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [uploadImageMutation, { data, loading, error }] = useUploadImageMutation({ - * variables: { - * file: // value for 'file' - * fileFolder: // value for 'fileFolder' - * }, - * }); - */ -export function useUploadImageMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UploadImageDocument, options); - } -export type UploadImageMutationHookResult = ReturnType; -export type UploadImageMutationResult = Apollo.MutationResult; -export type UploadImageMutationOptions = Apollo.BaseMutationOptions; export const AuthorizeAppDocument = gql` mutation authorizeApp($clientId: String!, $codeChallenge: String, $redirectUrl: String!) { authorizeApp( @@ -10972,78 +10860,6 @@ export function useFindOnePageLayoutLazyQuery(baseOptions?: Apollo.LazyQueryHook export type FindOnePageLayoutQueryHookResult = ReturnType; export type FindOnePageLayoutLazyQueryHookResult = ReturnType; export type FindOnePageLayoutQueryResult = Apollo.QueryResult; -export const CreateFileDocument = gql` - mutation CreateFile($file: Upload!) { - createFile(file: $file) { - id - path - size - createdAt - } -} - `; -export type CreateFileMutationFn = Apollo.MutationFunction; - -/** - * __useCreateFileMutation__ - * - * To run a mutation, you first call `useCreateFileMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useCreateFileMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [createFileMutation, { data, loading, error }] = useCreateFileMutation({ - * variables: { - * file: // value for 'file' - * }, - * }); - */ -export function useCreateFileMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(CreateFileDocument, options); - } -export type CreateFileMutationHookResult = ReturnType; -export type CreateFileMutationResult = Apollo.MutationResult; -export type CreateFileMutationOptions = Apollo.BaseMutationOptions; -export const DeleteFileDocument = gql` - mutation DeleteFile($fileId: UUID!) { - deleteFile(fileId: $fileId) { - id - path - size - createdAt - } -} - `; -export type DeleteFileMutationFn = Apollo.MutationFunction; - -/** - * __useDeleteFileMutation__ - * - * To run a mutation, you first call `useDeleteFileMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useDeleteFileMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [deleteFileMutation, { data, loading, error }] = useDeleteFileMutation({ - * variables: { - * fileId: // value for 'fileId' - * }, - * }); - */ -export function useDeleteFileMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(DeleteFileDocument, options); - } -export type DeleteFileMutationHookResult = ReturnType; -export type DeleteFileMutationResult = Apollo.MutationResult; -export type DeleteFileMutationOptions = Apollo.BaseMutationOptions; export const UploadFilesFieldFileDocument = gql` mutation UploadFilesFieldFile($file: Upload!, $fieldMetadataId: String!) { uploadFilesFieldFile(file: $file, fieldMetadataId: $fieldMetadataId) { @@ -14311,40 +14127,6 @@ export function useUploadWorkspaceMemberProfilePictureMutation(baseOptions?: Apo export type UploadWorkspaceMemberProfilePictureMutationHookResult = ReturnType; export type UploadWorkspaceMemberProfilePictureMutationResult = Apollo.MutationResult; export type UploadWorkspaceMemberProfilePictureMutationOptions = Apollo.BaseMutationOptions; -export const UploadWorkspaceMemberProfilePictureLegacyDocument = gql` - mutation UploadWorkspaceMemberProfilePictureLegacy($file: Upload!) { - uploadWorkspaceMemberProfilePictureLegacy(file: $file) { - path - token - } -} - `; -export type UploadWorkspaceMemberProfilePictureLegacyMutationFn = Apollo.MutationFunction; - -/** - * __useUploadWorkspaceMemberProfilePictureLegacyMutation__ - * - * To run a mutation, you first call `useUploadWorkspaceMemberProfilePictureLegacyMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUploadWorkspaceMemberProfilePictureLegacyMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [uploadWorkspaceMemberProfilePictureLegacyMutation, { data, loading, error }] = useUploadWorkspaceMemberProfilePictureLegacyMutation({ - * variables: { - * file: // value for 'file' - * }, - * }); - */ -export function useUploadWorkspaceMemberProfilePictureLegacyMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UploadWorkspaceMemberProfilePictureLegacyDocument, options); - } -export type UploadWorkspaceMemberProfilePictureLegacyMutationHookResult = ReturnType; -export type UploadWorkspaceMemberProfilePictureLegacyMutationResult = Apollo.MutationResult; -export type UploadWorkspaceMemberProfilePictureLegacyMutationOptions = Apollo.BaseMutationOptions; export const UpdateUserEmailDocument = gql` mutation UpdateUserEmail($newEmail: String!, $verifyEmailRedirectPath: String) { updateUserEmail( @@ -16948,40 +16730,6 @@ export function useUploadWorkspaceLogoMutation(baseOptions?: Apollo.MutationHook export type UploadWorkspaceLogoMutationHookResult = ReturnType; export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult; export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions; -export const UploadWorkspaceLogoLegacyDocument = gql` - mutation UploadWorkspaceLogoLegacy($file: Upload!) { - uploadWorkspaceLogoLegacy(file: $file) { - path - token - } -} - `; -export type UploadWorkspaceLogoLegacyMutationFn = Apollo.MutationFunction; - -/** - * __useUploadWorkspaceLogoLegacyMutation__ - * - * To run a mutation, you first call `useUploadWorkspaceLogoLegacyMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUploadWorkspaceLogoLegacyMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [uploadWorkspaceLogoLegacyMutation, { data, loading, error }] = useUploadWorkspaceLogoLegacyMutation({ - * variables: { - * file: // value for 'file' - * }, - * }); - */ -export function useUploadWorkspaceLogoLegacyMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(UploadWorkspaceLogoLegacyDocument, options); - } -export type UploadWorkspaceLogoLegacyMutationHookResult = ReturnType; -export type UploadWorkspaceLogoLegacyMutationResult = Apollo.MutationResult; -export type UploadWorkspaceLogoLegacyMutationOptions = Apollo.BaseMutationOptions; export const CheckCustomDomainValidRecordsDocument = gql` mutation CheckCustomDomainValidRecords { checkCustomDomainValidRecords { diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx index 30dc61fe27..48d78edddc 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx @@ -10,22 +10,23 @@ import { downloadFile } from '@/activities/files/utils/downloadFile'; import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState'; import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper'; -import { ModalContent, ModalHeader } from 'twenty-ui/layout'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { ModalContent, ModalHeader } from 'twenty-ui/layout'; import { ActivityList } from '@/activities/components/ActivityList'; +import { + type AttachmentWithFile, + filterAttachmentsWithFile, +} from '@/activities/files/utils/filterAttachmentsWithFile'; +import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; -import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { isDefined } from 'twenty-shared/utils'; import { IconDownload, IconX } from 'twenty-ui/display'; import { IconButton } from 'twenty-ui/input'; import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { - PermissionFlagType, - FeatureFlagKey, -} from '~/generated-metadata/graphql'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; import { AttachmentRow } from './AttachmentRow'; const DocumentViewer = lazy(() => @@ -122,16 +123,12 @@ export const AttachmentList = ({ const { uploadAttachmentFile } = useUploadAttachmentFile(); const [isDraggingFile, setIsDraggingFile] = useState(false); const [previewedAttachment, setPreviewedAttachment] = - useState(null); + useState(null); const isAttachmentPreviewEnabled = useAtomStateValue( isAttachmentPreviewEnabledState, ); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); - const hasDownloadPermission = useHasPermissionFlag( PermissionFlagType.DOWNLOAD_FILE, ); @@ -142,15 +139,7 @@ export const AttachmentList = ({ const { openModal, closeModal } = useModal(); - const getAttachmentUrl = (attachment: Attachment) => { - const fileUrl = isFilesFieldMigrated - ? attachment.file?.[0]?.url || attachment.fullPath - : attachment.fullPath; - - assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`)); - - return fileUrl; - }; + const attachmentsWithFile = filterAttachmentsWithFile(attachments); const onUploadFile = async (file: File) => { await uploadAttachmentFile(file, targetableObject); @@ -162,7 +151,7 @@ export const AttachmentList = ({ } }; - const handlePreview = (attachment: Attachment) => { + const handlePreview = (attachment: AttachmentWithFile) => { if (!isAttachmentPreviewEnabled) return; setPreviewedAttachment(attachment); openModal(PREVIEW_MODAL_ID); @@ -174,20 +163,18 @@ export const AttachmentList = ({ }; const handleDownload = () => { - if (!previewedAttachment) return; - downloadFile( - getAttachmentUrl(previewedAttachment), - previewedAttachment.name, - ); + if (!isDefined(previewedAttachment)) return; + const attachmentUrl = getAttachmentUrl({ attachment: previewedAttachment }); + downloadFile(attachmentUrl, previewedAttachment.name); }; return ( <> - {attachments && attachments.length > 0 && ( + {attachmentsWithFile && attachmentsWithFile.length > 0 && ( - {title} {attachments.length} + {title} {attachmentsWithFile.length} {button} @@ -201,7 +188,7 @@ export const AttachmentList = ({ /> ) : ( - {attachments.map((attachment) => ( + {attachmentsWithFile.map((attachment) => ( 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 e908f8e8a3..5276def634 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -1,8 +1,6 @@ import { ActivityRow } from '@/activities/components/ActivityRow'; import { AttachmentDropdown } from '@/activities/files/components/AttachmentDropdown'; -import { type Attachment } from '@/activities/files/types/Attachment'; import { downloadFile } from '@/activities/files/utils/downloadFile'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { @@ -11,21 +9,19 @@ import { } from '@/object-record/record-field/ui/contexts/FieldContext'; import { getFileCategoryFromExtension } from '@/object-record/record-field/ui/utils/getFileCategoryFromExtension'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useContext, useState } from 'react'; import { styled } from '@linaria/react'; +import { useContext, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; +import { type AttachmentWithFile } from '@/activities/files/utils/filterAttachmentsWithFile'; import { FileIcon } from '@/file/components/FileIcon'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display'; import { ThemeContext } from 'twenty-ui/theme'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { isNavigationModifierPressed } from 'twenty-ui/utilities'; -import { - FeatureFlagKey, - PermissionFlagType, -} from '~/generated-metadata/graphql'; +import { PermissionFlagType } from '~/generated-metadata/graphql'; import { formatToHumanReadableDate } from '~/utils/date-utils'; import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension'; @@ -77,8 +73,8 @@ const StyledLinkContainer = styled.div` `; type AttachmentRowProps = { - attachment: Attachment; - onPreview?: (attachment: Attachment) => void; + attachment: AttachmentWithFile; + onPreview?: (attachment: AttachmentWithFile) => void; }; export const AttachmentRow = ({ @@ -88,31 +84,19 @@ export const AttachmentRow = ({ const { theme } = useContext(ThemeContext); const [isEditing, setIsEditing] = useState(false); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); - const hasDownloadPermission = useHasPermissionFlag( PermissionFlagType.DOWNLOAD_FILE, ); const { name: originalFileName, extension: attachmentFileExtension } = - getFileNameAndExtension( - isFilesFieldMigrated - ? (attachment.file?.[0]?.label as string) - : attachment.name, - ); + getFileNameAndExtension(attachment.file.label); const [attachmentFileName, setAttachmentFileName] = useState(originalFileName); - const fileCategory = isFilesFieldMigrated - ? getFileCategoryFromExtension(attachment.file?.[0]?.extension) - : attachment.fileCategory; + const fileCategory = getFileCategoryFromExtension(attachment.file.extension); - const fileUrl = isFilesFieldMigrated - ? (attachment.file?.[0]?.url as string) // TODO : fix attachment.file type after Files field migration - : attachment.fullPath; + const fileUrl = attachment.file.url; const { destroyOneRecord: destroyOneAttachment } = useDestroyOneRecord({ objectNameSingular: CoreObjectNameSingular.Attachment, @@ -138,16 +122,12 @@ export const AttachmentRow = ({ idToUpdate: attachment.id, updateOneRecordInput: { name: newFileName, - ...(isFilesFieldMigrated && isDefined(attachment.file?.[0]?.fileId) - ? { - file: [ - { - fileId: attachment.file?.[0]?.fileId, - label: newFileName, - }, - ], - } - : {}), + file: [ + { + fileId: attachment.file.fileId, + label: newFileName, + }, + ], }, }); }; diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx index d02da78f80..3b006d90f4 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx @@ -1,5 +1,4 @@ import { type Attachment } from '@/activities/files/types/Attachment'; -import { getFileType } from '@/activities/files/utils/getFileType'; import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -12,23 +11,17 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { FeatureFlagKey, FieldMetadataType, - FileFolder, - useUploadFileMutation, useUploadFilesFieldFileMutation, } from '~/generated-metadata/graphql'; export const useUploadAttachmentFile = () => { const apolloClient = useApolloClient(); - const [uploadFile] = useUploadFileMutation({ client: apolloClient }); const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({ client: apolloClient, }); const isAttachmentMigrated = useIsFeatureEnabled( FeatureFlagKey.IS_ATTACHMENT_MIGRATED, ); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({ objectNameSingular: CoreObjectNameSingular.Attachment, @@ -48,44 +41,19 @@ export const useUploadAttachmentFile = () => { file: File, targetableObject: ActivityTargetableObject, ) => { - let attachmentPath: string; - let fileId: string | undefined; - let fileUrl: string | undefined; + assertIsDefinedOrThrow( + filesFieldMetadataId, + new Error(t`File field not found for attachment object`), + ); - if (isFilesFieldMigrated) { - assertIsDefinedOrThrow( - filesFieldMetadataId, - new Error(t`File field not found for attachment object`), - ); + const result = await uploadFilesFieldFile({ + variables: { file, fieldMetadataId: filesFieldMetadataId }, + }); - const result = await uploadFilesFieldFile({ - variables: { file, fieldMetadataId: filesFieldMetadataId }, - }); + const uploadedFile = result?.data?.uploadFilesFieldFile; - const uploadedFile = result?.data?.uploadFilesFieldFile; - - if (!isDefined(uploadedFile)) { - throw new Error("Couldn't upload the attachment."); - } - - attachmentPath = uploadedFile.path; - fileId = uploadedFile.id; - fileUrl = uploadedFile.url; - } else { - const result = await uploadFile({ - variables: { - file, - fileFolder: FileFolder.Attachment, - }, - }); - - const signedFile = result?.data?.uploadFile; - - if (!isDefined(signedFile)) { - throw new Error("Couldn't upload the attachment."); - } - - attachmentPath = signedFile.path; + if (!isDefined(uploadedFile)) { + throw new Error("Couldn't upload the attachment."); } const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ @@ -95,28 +63,20 @@ export const useUploadAttachmentFile = () => { const attachmentToCreate = { name: file.name, - fullPath: isFilesFieldMigrated ? null : attachmentPath, - fileCategory: getFileType(file.name), [targetableObjectFieldIdName]: targetableObject.id, - ...(isFilesFieldMigrated && isDefined(fileId) - ? { - file: [ - { - fileId, - label: file.name, - }, - ], - } - : {}), + file: [ + { + fileId: uploadedFile.id, + label: file.name, + }, + ], } as Partial; - const createdAttachment = await createOneAttachment(attachmentToCreate); + await createOneAttachment(attachmentToCreate); return { - attachmentAbsoluteURL: isFilesFieldMigrated - ? fileUrl - : createdAttachment.fullPath, - attachmentFileId: fileId, + attachmentAbsoluteURL: uploadedFile.url, + attachmentFileId: uploadedFile.id, }; }; 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 02ac27c32d..394721fc49 100644 --- a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts +++ b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts @@ -7,9 +7,11 @@ export type { AttachmentFileCategory }; export type Attachment = { id: string; name: string; + /** @deprecated Use `file[0].url` field instead */ fullPath: string; + /** @deprecated Use `file[0].extension` field instead */ fileCategory: AttachmentFileCategory; - file?: FieldFilesValue[] | null; + file: FieldFilesValue[] | null; companyId?: string | null; personId?: string | null; taskId?: string | null; diff --git a/packages/twenty-front/src/modules/activities/files/utils/filterAttachmentsWithFile.ts b/packages/twenty-front/src/modules/activities/files/utils/filterAttachmentsWithFile.ts new file mode 100644 index 0000000000..220666669a --- /dev/null +++ b/packages/twenty-front/src/modules/activities/files/utils/filterAttachmentsWithFile.ts @@ -0,0 +1,34 @@ +import { type Attachment } from '@/activities/files/types/Attachment'; +import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; + +export type FieldFilesValueWithUrl = FieldFilesValue & { + url: string; +}; + +type AttachmentWithFiles = Attachment & { + file: [FieldFilesValueWithUrl, ...FieldFilesValueWithUrl[]]; +}; + +export type AttachmentWithFile = Omit & { + file: FieldFilesValueWithUrl; +}; + +const hasFileWithUrl = ( + attachment: Attachment, +): attachment is AttachmentWithFiles => { + return isNonEmptyArray(attachment.file) && isDefined(attachment.file[0].url); +}; + +const normalizeAttachment = ( + attachment: AttachmentWithFiles, +): AttachmentWithFile => ({ + ...attachment, + file: attachment.file[0], +}); + +export const filterAttachmentsWithFile = ( + attachments: Attachment[], +): AttachmentWithFile[] => { + return attachments.filter(hasFileWithUrl).map(normalizeAttachment); +}; diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts index dfcbc06cdd..a6eed92f12 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/filterAttachmentsToRestore.test.ts @@ -6,13 +6,12 @@ describe('filterAttachmentsToRestore', () => { const softDeletedAttachments = [ { id: '1', - fullPath: 'https://exemple.com/test.txt', + file: [{ url: 'https://exemple.com/test.txt' }], }, ] as Attachment[]; const attachmentIdsToRestore = filterAttachmentsToRestore({ attachmentPathsToRestore: [], softDeletedAttachments, - isFilesFieldMigrated: false, }); expect(attachmentIdsToRestore).toEqual([]); }); @@ -23,7 +22,6 @@ describe('filterAttachmentsToRestore', () => { 'https://exemple.com/files/attachment/test.txt', ], softDeletedAttachments: [], - isFilesFieldMigrated: false, }); expect(attachmentIdsToRestore).toEqual([]); }); @@ -32,17 +30,16 @@ describe('filterAttachmentsToRestore', () => { const softDeletedAttachments = [ { id: '1', - fullPath: 'https://exemple.com/files/images/test.txt', + file: [{ url: 'https://exemple.com/files/images/test.txt' }], }, { id: '2', - fullPath: 'https://exemple.com/files/images/test2.txt', + file: [{ url: 'https://exemple.com/files/images/test2.txt' }], }, ] as Attachment[]; const attachmentIdsToRestore = filterAttachmentsToRestore({ attachmentPathsToRestore: ['https://exemple.com/files/images/test.txt'], softDeletedAttachments, - isFilesFieldMigrated: false, }); expect(attachmentIdsToRestore).toEqual(['1']); }); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts index 25c72b7ff9..81de973368 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsAndNameToUpdate.test.ts @@ -6,12 +6,12 @@ describe('getActivityAttachmentIdsAndNameToUpdate', () => { const attachments = [ { id: '1', - fullPath: 'https://exemple.com/files/images/test.txt', + file: [{ url: 'https://exemple.com/files/images/test.txt' }], name: 'image', }, { id: '2', - fullPath: 'https://exemple.com/files/images/test2.txt', + file: [{ url: 'https://exemple.com/files/images/test2.txt' }], name: 'image1', }, ] as Attachment[]; @@ -33,7 +33,7 @@ describe('getActivityAttachmentIdsAndNameToUpdate', () => { }, ]); const attachmentIdsAndNameToUpdate = - getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments, false); + getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments); expect(attachmentIdsAndNameToUpdate).toEqual([]); }); @@ -41,12 +41,12 @@ describe('getActivityAttachmentIdsAndNameToUpdate', () => { const attachments = [ { id: '1', - fullPath: 'https://exemple.com/files/images/test.txt', + file: [{ url: 'https://exemple.com/files/images/test.txt' }], name: 'image', }, { id: '2', - fullPath: 'https://exemple.com/files/images/test2.txt', + file: [{ url: 'https://exemple.com/files/images/test2.txt' }], name: 'image1', }, ] as Attachment[]; @@ -68,7 +68,7 @@ describe('getActivityAttachmentIdsAndNameToUpdate', () => { }, ]); const attachmentIdsAndNameToUpdate = - getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments, false); + getActivityAttachmentIdsAndNameToUpdate(activityBody, attachments); expect(attachmentIdsAndNameToUpdate).toEqual([{ id: '2', name: 'image4' }]); }); }); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts index 623d6fd9d8..31e1266852 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentIdsToDelete.test.ts @@ -6,11 +6,11 @@ describe('getActivityAttachmentIdsToDelete', () => { const attachments = [ { id: '1', - fullPath: 'https://example.com/files/images/test.txt', + file: [{ url: 'https://example.com/files/images/test.txt' }], }, { id: '2', - fullPath: 'https://example.com/files/images/test2.txt', + file: [{ url: 'https://example.com/files/images/test2.txt' }], }, ] as Attachment[]; const newActivityBody = JSON.stringify([ @@ -37,7 +37,6 @@ describe('getActivityAttachmentIdsToDelete', () => { newActivityBody, attachments, oldActivityBody, - false, ); expect(attachmentIdsToDelete).toEqual([]); }); @@ -46,11 +45,11 @@ describe('getActivityAttachmentIdsToDelete', () => { const attachments = [ { id: '1', - fullPath: 'https://example.com/files/images/test.txt', + file: [{ url: 'https://example.com/files/images/test.txt' }], }, { id: '2', - fullPath: 'https://example.com/files/images/test2.txt', + file: [{ url: 'https://example.com/files/images/test2.txt' }], }, ] as Attachment[]; const newActivityBody = JSON.stringify([ @@ -73,7 +72,6 @@ describe('getActivityAttachmentIdsToDelete', () => { newActivityBody, attachments, oldActivityBody, - false, ); expect(attachmentIdsToDelete).toEqual(['2']); }); diff --git a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts index 88eeef3d22..675c606288 100644 --- a/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts +++ b/packages/twenty-front/src/modules/activities/utils/__tests__/getActivityAttachmentPathsToRestore.test.ts @@ -17,7 +17,6 @@ describe('getActivityAttachmentPathsToRestore', () => { const attachmentPathsToRestore = getActivityAttachmentPathsToRestore( newActivityBody, oldActivityAttachments, - false, ); expect(attachmentPathsToRestore).toEqual([]); }); @@ -37,14 +36,13 @@ describe('getActivityAttachmentPathsToRestore', () => { const oldActivityAttachments = [ { id: '1', - fullPath: 'https://example.com/files/images/test.txt', + file: [{ url: 'https://example.com/files/images/test.txt' }], }, ] as Attachment[]; const attachmentPathsToRestore = getActivityAttachmentPathsToRestore( newActivityBody, oldActivityAttachments, - false, ); expect(attachmentPathsToRestore).toEqual([ 'https://example.com/files/images/test2.txt', diff --git a/packages/twenty-front/src/modules/activities/utils/compareUrls.ts b/packages/twenty-front/src/modules/activities/utils/compareUrls.ts index 8e778e20c9..2ed833829e 100644 --- a/packages/twenty-front/src/modules/activities/utils/compareUrls.ts +++ b/packages/twenty-front/src/modules/activities/utils/compareUrls.ts @@ -1,9 +1,14 @@ import { getAttachmentPath } from '@/activities/utils/getAttachmentPath'; +import { isDefined } from 'twenty-shared/utils'; export const compareUrls = ( - firstAttachmentUrl: string, - secondAttachmentUrl: string, + firstAttachmentUrl: string | undefined, + secondAttachmentUrl: string | undefined, ): boolean => { + if (!isDefined(firstAttachmentUrl) || !isDefined(secondAttachmentUrl)) { + return false; + } + try { const urlA = new URL(firstAttachmentUrl); const urlB = new URL(secondAttachmentUrl); diff --git a/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts b/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts index 6e59ccf100..e76af7c9e0 100644 --- a/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts +++ b/packages/twenty-front/src/modules/activities/utils/filterAttachmentsToRestore.ts @@ -1,23 +1,19 @@ import { type Attachment } from '@/activities/files/types/Attachment'; +import { filterAttachmentsWithFile } from '@/activities/files/utils/filterAttachmentsWithFile'; import { compareUrls } from '@/activities/utils/compareUrls'; import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl'; export const filterAttachmentsToRestore = ({ attachmentPathsToRestore, softDeletedAttachments, - isFilesFieldMigrated, }: { attachmentPathsToRestore: string[]; softDeletedAttachments: Attachment[]; - isFilesFieldMigrated: boolean; }) => { - return softDeletedAttachments + return filterAttachmentsWithFile(softDeletedAttachments) .filter((attachment) => attachmentPathsToRestore.some((path) => - compareUrls( - getAttachmentUrl({ attachment, isFilesFieldMigrated }), - path, - ), + compareUrls(getAttachmentUrl({ attachment }), path), ), ) .map((attachment) => attachment.id); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts index 276eed9f5f..4207ad8f8c 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsAndNameToUpdate.ts @@ -1,4 +1,5 @@ import { type Attachment } from '@/activities/files/types/Attachment'; +import { filterAttachmentsWithFile } from '@/activities/files/utils/filterAttachmentsWithFile'; import { compareUrls } from '@/activities/utils/compareUrls'; import { type AttachmentInfo, @@ -10,19 +11,17 @@ import { isDefined } from 'twenty-shared/utils'; export const getActivityAttachmentIdsAndNameToUpdate = ( newActivityBody: string, oldActivityAttachments: Attachment[] = [], - isFilesFieldMigrated: boolean, ) => { const activityAttachmentsNameAndPaths = getActivityAttachmentPathsAndName(newActivityBody); if (activityAttachmentsNameAndPaths.length === 0) return []; + const attachmentsWithFile = filterAttachmentsWithFile(oldActivityAttachments); + return activityAttachmentsNameAndPaths.reduce( (acc: Partial[], activity: AttachmentInfo) => { - const foundActivity = oldActivityAttachments.find((attachment) => - compareUrls( - getAttachmentUrl({ attachment, isFilesFieldMigrated }), - activity.path, - ), + const foundActivity = attachmentsWithFile.find((attachment) => + compareUrls(getAttachmentUrl({ attachment }), activity.path), ); if (isDefined(foundActivity) && foundActivity.name !== activity.name) { acc.push({ id: foundActivity.id, name: activity.name }); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts index 8042bca8ec..8ed9c3749c 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts @@ -1,4 +1,5 @@ import { type Attachment } from '@/activities/files/types/Attachment'; +import { filterAttachmentsWithFile } from '@/activities/files/utils/filterAttachmentsWithFile'; import { compareUrls } from '@/activities/utils/compareUrls'; import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName'; import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl'; @@ -7,7 +8,6 @@ export const getActivityAttachmentIdsToDelete = ( newActivityBody: string, oldActivityAttachments: Attachment[] = [], oldActivityBody: string, - isFilesFieldMigrated: boolean, ) => { if (oldActivityAttachments.length === 0) return []; @@ -26,13 +26,10 @@ export const getActivityAttachmentIdsToDelete = ( ) .map((activity) => activity.path); - return oldActivityAttachments + return filterAttachmentsWithFile(oldActivityAttachments) .filter((attachment) => pathsToDelete.some((pathToDelete) => - compareUrls( - getAttachmentUrl({ attachment, isFilesFieldMigrated }), - pathToDelete, - ), + compareUrls(getAttachmentUrl({ attachment }), pathToDelete), ), ) .map((attachment) => attachment.id); diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts index d65c6e855c..128a6913b6 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentPathsToRestore.ts @@ -1,4 +1,5 @@ import { type Attachment } from '@/activities/files/types/Attachment'; +import { filterAttachmentsWithFile } from '@/activities/files/utils/filterAttachmentsWithFile'; import { compareUrls } from '@/activities/utils/compareUrls'; import { getActivityAttachmentPathsAndName } from '@/activities/utils/getActivityAttachmentPathsAndName'; import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl'; @@ -6,19 +7,17 @@ import { getAttachmentUrl } from '@/activities/utils/getAttachmentUrl'; export const getActivityAttachmentPathsToRestore = ( newActivityBody: string, oldActivityAttachments: Attachment[], - isFilesFieldMigrated: boolean, ) => { const newActivityAttachmentPaths = getActivityAttachmentPathsAndName(newActivityBody); + const attachmentsWithFile = filterAttachmentsWithFile(oldActivityAttachments); + const pathsToRestore = newActivityAttachmentPaths .filter( (newActivity) => - !oldActivityAttachments.some((attachment) => - compareUrls( - newActivity.path, - getAttachmentUrl({ attachment, isFilesFieldMigrated }), - ), + !attachmentsWithFile.some((attachment) => + compareUrls(newActivity.path, getAttachmentUrl({ attachment })), ), ) .map((activity) => activity.path); diff --git a/packages/twenty-front/src/modules/activities/utils/getAttachmentUrl.ts b/packages/twenty-front/src/modules/activities/utils/getAttachmentUrl.ts index e229897df1..72e1d30cd7 100644 --- a/packages/twenty-front/src/modules/activities/utils/getAttachmentUrl.ts +++ b/packages/twenty-front/src/modules/activities/utils/getAttachmentUrl.ts @@ -1,16 +1,9 @@ -import { type Attachment } from '@/activities/files/types/Attachment'; +import { type AttachmentWithFile } from '@/activities/files/utils/filterAttachmentsWithFile'; export const getAttachmentUrl = ({ attachment, - isFilesFieldMigrated, }: { - attachment: Attachment; - isFilesFieldMigrated: boolean; + attachment: AttachmentWithFile; }): string => { - if (isFilesFieldMigrated) { - //TODO : add minimumFile settings + set it for attachment.file files field + exception invariance check here - return attachment.file?.[0]?.url as string; - } - - return attachment.fullPath as string; + return attachment.file.url; }; diff --git a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts index 33a743157d..81a073c538 100644 --- a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts +++ b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useUploadWorkflowFile.ts @@ -1,28 +1,17 @@ import { MAX_ATTACHMENT_SIZE } from '@/advanced-text-editor/utils/MaxAttachmentSize'; import { formatFileSize } from '@/file/utils/formatFileSize'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useApolloClient } from '@apollo/client'; import { t } from '@lingui/core/macro'; import { extractFolderPathFilenameAndTypeOrThrow, isDefined, } from 'twenty-shared/utils'; import { type WorkflowAttachment } from 'twenty-shared/workflow'; -import { - FeatureFlagKey, - useCreateFileMutation, - useUploadWorkflowFileMutation, -} from '~/generated-metadata/graphql'; +import { useUploadWorkflowFileMutation } from '~/generated-metadata/graphql'; import { logError } from '~/utils/logError'; export const useUploadWorkflowFile = () => { - const isOtherFileMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_OTHER_FILE_MIGRATED, - ); const [uploadWorkflowFileMutation] = useUploadWorkflowFileMutation(); - const apolloClient = useApolloClient(); - const [createFile] = useCreateFileMutation({ client: apolloClient }); const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); const uploadWorkflowFile = async ( @@ -38,41 +27,20 @@ export const useUploadWorkflowFile = () => { return null; } - let workflowFile: WorkflowAttachment; - if (isOtherFileMigrated) { - const result = await uploadWorkflowFileMutation({ - variables: { file }, - }); - const uploadedFile = result?.data?.uploadWorkflowFile; - if (!isDefined(uploadedFile)) { - throw new Error('File upload failed'); - } - workflowFile = { - id: uploadedFile.id, - name: file.name, - size: uploadedFile.size, - type: extractFolderPathFilenameAndTypeOrThrow(uploadedFile.path).type, - createdAt: uploadedFile.createdAt, - }; - } else { - const result = await createFile({ - variables: { file }, - }); - - const uploadedFile = result?.data?.createFile; - - if (!isDefined(uploadedFile)) { - throw new Error('File upload failed'); - } - - workflowFile = { - id: uploadedFile.id, - name: file.name, - size: uploadedFile.size, - type: extractFolderPathFilenameAndTypeOrThrow(uploadedFile.path).type, - createdAt: uploadedFile.createdAt, - }; + const result = await uploadWorkflowFileMutation({ + variables: { file }, + }); + const uploadedFile = result?.data?.uploadWorkflowFile; + if (!isDefined(uploadedFile)) { + throw new Error('File upload failed'); } + const workflowFile: WorkflowAttachment = { + id: uploadedFile.id, + name: file.name, + size: uploadedFile.size, + type: extractFolderPathFilenameAndTypeOrThrow(uploadedFile.path).type, + createdAt: uploadedFile.createdAt, + }; const fileName = file.name; enqueueSuccessSnackBar({ diff --git a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx index ebe29a905c..95820d19d7 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx @@ -7,7 +7,7 @@ import { AIChatAssistantMessageRenderer } from '@/ai/components/AIChatAssistantM import { AIChatErrorRenderer } from '@/ai/components/AIChatErrorRenderer'; import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; -import { type ExtendedUIMessage } from 'twenty-shared/ai'; +import { isExtendedFileUIPart, type ExtendedUIMessage } from 'twenty-shared/ai'; import { isDefined } from 'twenty-shared/utils'; import { themeCssVariables } from 'twenty-ui/theme-constants'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; @@ -151,7 +151,7 @@ export const AIChatMessage = ({ const showError = isDefined(error) && message.role === AgentMessageRole.ASSISTANT; - const fileParts = message.parts.filter((part) => part.type === 'file'); + const fileParts = message.parts.filter(isExtendedFileUIPart); return ( diff --git a/packages/twenty-front/src/modules/ai/components/CodeExecutionDisplay.tsx b/packages/twenty-front/src/modules/ai/components/CodeExecutionDisplay.tsx index a876d4313e..7c20c6df8d 100644 --- a/packages/twenty-front/src/modules/ai/components/CodeExecutionDisplay.tsx +++ b/packages/twenty-front/src/modules/ai/components/CodeExecutionDisplay.tsx @@ -176,6 +176,7 @@ type CodeExecutionDisplayProps = { stderr: string; exitCode?: number; files?: Array<{ + fileId: string; filename: string; url: string; mimeType?: string; @@ -337,7 +338,7 @@ export const CodeExecutionDisplay = ({ const filename = file.filename; return ( - + {isPreviewableMimeType(file.mimeType) ? ( ; + files?: Array<{ + fileId: string; + filename: string; + url: string; + mimeType?: string; + }>; }; } | null; diff --git a/packages/twenty-front/src/modules/ai/components/__stories__/AIChatMessage.stories.tsx b/packages/twenty-front/src/modules/ai/components/__stories__/AIChatMessage.stories.tsx index 5683cd18a9..aec40c4e44 100644 --- a/packages/twenty-front/src/modules/ai/components/__stories__/AIChatMessage.stories.tsx +++ b/packages/twenty-front/src/modules/ai/components/__stories__/AIChatMessage.stories.tsx @@ -66,6 +66,7 @@ print("Chart saved successfully!")`, executionTimeMs: 2340, files: [ { + fileId: '550e8400-e29b-41d4-a716-446655440005', filename: 'sales_chart.png', url: 'https://picsum.photos/800/480', mimeType: 'image/png', diff --git a/packages/twenty-front/src/modules/ai/components/__stories__/CodeExecutionDisplay.stories.tsx b/packages/twenty-front/src/modules/ai/components/__stories__/CodeExecutionDisplay.stories.tsx index 9fa676844d..db32957cdb 100644 --- a/packages/twenty-front/src/modules/ai/components/__stories__/CodeExecutionDisplay.stories.tsx +++ b/packages/twenty-front/src/modules/ai/components/__stories__/CodeExecutionDisplay.stories.tsx @@ -103,11 +103,13 @@ export const WithImageFiles: Story = { isRunning: false, files: [ { + fileId: '550e8400-e29b-41d4-a716-446655440001', filename: 'revenue_chart.png', url: 'https://picsum.photos/800/480', mimeType: 'image/png', }, { + fileId: '550e8400-e29b-41d4-a716-446655440002', filename: 'pie_chart.png', url: 'https://picsum.photos/600/400', mimeType: 'image/png', @@ -142,11 +144,13 @@ print("Files exported successfully!")`, isRunning: false, files: [ { + fileId: '550e8400-e29b-41d4-a716-446655440003', filename: 'report.csv', url: 'data:text/csv,name%2Csales%0AAlice%2C1200%0ABob%2C1500', mimeType: 'text/csv', }, { + fileId: '550e8400-e29b-41d4-a716-446655440004', filename: 'data.json', url: 'data:application/json,%7B%22name%22%3A%5B%22Alice%22%5D%7D', mimeType: 'application/json', 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 71c47331db..c9dd321474 100644 --- a/packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx +++ b/packages/twenty-front/src/modules/ai/components/internal/AgentChatFilePreview.tsx @@ -2,43 +2,66 @@ import { type AttachmentFileCategory } from '@/activities/files/types/Attachment import { getFileType } from '@/activities/files/utils/getFileType'; import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors'; import { IconMapping } from '@/file/utils/fileIconMappings'; +import { getFileCategoryFromExtension } from '@/object-record/record-field/ui/utils/getFileCategoryFromExtension'; +import { filePreviewState } from '@/ui/field/display/states/filePreviewState'; +import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; +import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; -import { useContext } from 'react'; -import { type FileUIPart } from 'ai'; +import { useCallback, useContext } from 'react'; +import { type ExtendedFileUIPart } from 'twenty-shared/ai'; import { isDefined } from 'twenty-shared/utils'; -import { - AvatarOrIcon, - Chip, - ChipVariant, - LinkChip, -} from 'twenty-ui/components'; +import { AvatarOrIcon, Chip, ChipVariant } from 'twenty-ui/components'; import { type IconComponent, IconX } from 'twenty-ui/display'; import { Loader } from 'twenty-ui/feedback'; import { ThemeContext } from 'twenty-ui/theme'; +const StyledClickableContainer = styled.div<{ clickable: boolean }>` + cursor: ${({ clickable }: { clickable: boolean }) => + clickable ? 'pointer' : 'inherit'}; + display: inline-flex; + min-width: 0; +`; + export const AgentChatFilePreview = ({ file, onRemove, isUploading, }: { - file: FileUIPart | File; + file: ExtendedFileUIPart | File; onRemove?: () => void; isUploading?: boolean; }) => { const { theme } = useContext(ThemeContext); const iconColors: Record = useFileCategoryColors(); + const setFilePreview = useSetAtomState(filePreviewState); const fileName = file instanceof File ? file.name : (file.filename ?? t`Unknown file`); const fileUrl = file instanceof File ? undefined : file.url; + const fileId = file instanceof File ? undefined : file.fileId; const fileCategory: AttachmentFileCategory = getFileType(fileName); + const extension = fileName.split('.').pop() ?? ''; const FileCategoryIcon: IconComponent = IconMapping[fileCategory]; const iconBackgroundColor: string = iconColors[fileCategory]; + const handleClick = useCallback(() => { + if (!isDefined(fileUrl) || !isDefined(fileId)) { + return; + } + + setFilePreview({ + fileId, + label: fileName, + extension, + url: fileUrl, + fileCategory: getFileCategoryFromExtension(extension), + }); + }, [fileUrl, fileId, fileName, extension, setFilePreview]); + const leftComponent = isUploading ? ( ) : ( @@ -57,31 +80,22 @@ export const AgentChatFilePreview = ({ ) : undefined; const hasRightDivider = isDefined(onRemove); + const isClickable = isDefined(fileUrl) && isDefined(fileId); - if (isDefined(fileUrl)) { - return ( - + - ); - } - - return ( - + ); }; diff --git a/packages/twenty-front/src/modules/ai/graphql/mutations/uploadAIChatFile.ts b/packages/twenty-front/src/modules/ai/graphql/mutations/uploadAIChatFile.ts new file mode 100644 index 0000000000..86eadd1d46 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/graphql/mutations/uploadAIChatFile.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; + +export const UPLOAD_AI_CHAT_FILE = gql` + mutation uploadAIChatFile($file: Upload!) { + uploadAIChatFile(file: $file) { + id + path + size + createdAt + url + } + } +`; diff --git a/packages/twenty-front/src/modules/ai/graphql/queries/getChatMessages.ts b/packages/twenty-front/src/modules/ai/graphql/queries/getChatMessages.ts index 0dff27ff1b..685dd94de1 100644 --- a/packages/twenty-front/src/modules/ai/graphql/queries/getChatMessages.ts +++ b/packages/twenty-front/src/modules/ai/graphql/queries/getChatMessages.ts @@ -32,6 +32,7 @@ export const GET_CHAT_MESSAGES = gql` fileMediaType fileFilename fileUrl + fileId providerMetadata createdAt } diff --git a/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts b/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts index 192e02cf7e..1a0e6c8b6d 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts @@ -4,17 +4,16 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useApolloClient } from '@apollo/client'; import { useLingui } from '@lingui/react/macro'; -import { type FileUIPart } from 'ai'; -import { buildSignedPath, isDefined } from 'twenty-shared/utils'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { - FileFolder, - useUploadFileMutation, -} from '~/generated-metadata/graphql'; +import { isDefined } from 'twenty-shared/utils'; + +import { type AgentChatFileUIPart } from '@/ai/types/agent-chat-file-ui-part.type'; +import { useUploadAiChatFileMutation } from '~/generated-metadata/graphql'; export const useAIChatFileUpload = () => { const apolloClient = useApolloClient(); - const [uploadFile] = useUploadFileMutation({ client: apolloClient }); + const [uploadAiChatFile] = useUploadAiChatFileMutation({ + client: apolloClient, + }); const { t } = useLingui(); const { enqueueErrorSnackBar } = useSnackBar(); const [agentChatSelectedFiles, setAgentChatSelectedFiles] = useAtomState( @@ -24,33 +23,28 @@ export const useAIChatFileUpload = () => { agentChatUploadedFilesState, ); - const sendFile = async (file: File): Promise => { + const sendFile = async (file: File): Promise => { try { - const result = await uploadFile({ + const result = await uploadAiChatFile({ variables: { file, - fileFolder: FileFolder.AgentChat, }, }); - const response = result?.data?.uploadFile; + const response = result?.data?.uploadAIChatFile; if (!isDefined(response)) { throw new Error(t`Couldn't upload the file.`); } - const signedPath = buildSignedPath({ - path: response.path, - token: response.token, - }); - setAgentChatSelectedFiles( agentChatSelectedFiles.filter((f) => f.name !== file.name), ); return { filename: file.name, mediaType: file.type, - url: `${REACT_APP_SERVER_BASE_URL}/files/${signedPath}`, + url: response.url, + fileId: response.id, type: 'file', }; } catch { @@ -67,7 +61,7 @@ export const useAIChatFileUpload = () => { files.map((file) => sendFile(file)), ); - const successfulUploads = uploadResults.reduce( + const successfulUploads = uploadResults.reduce( (acc, result) => { if (result.status === 'fulfilled' && isDefined(result.value)) { acc.push(result.value); diff --git a/packages/twenty-front/src/modules/ai/states/agentChatUploadedFilesState.ts b/packages/twenty-front/src/modules/ai/states/agentChatUploadedFilesState.ts index 33cf1ad5af..3bc49cf868 100644 --- a/packages/twenty-front/src/modules/ai/states/agentChatUploadedFilesState.ts +++ b/packages/twenty-front/src/modules/ai/states/agentChatUploadedFilesState.ts @@ -1,8 +1,10 @@ -import { type FileUIPart } from 'ai'; - import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState'; -export const agentChatUploadedFilesState = createAtomState({ +import { type AgentChatFileUIPart } from '@/ai/types/agent-chat-file-ui-part.type'; + +export const agentChatUploadedFilesState = createAtomState< + AgentChatFileUIPart[] +>({ key: 'ai/agentChatUploadedFilesState', defaultValue: [], }); diff --git a/packages/twenty-front/src/modules/ai/types/agent-chat-file-ui-part.type.ts b/packages/twenty-front/src/modules/ai/types/agent-chat-file-ui-part.type.ts new file mode 100644 index 0000000000..76cad9e364 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/types/agent-chat-file-ui-part.type.ts @@ -0,0 +1,5 @@ +import { type FileUIPart } from 'ai'; + +export type AgentChatFileUIPart = FileUIPart & { + fileId: string; +}; diff --git a/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts b/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts index ccef19433f..d30297f404 100644 --- a/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts +++ b/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts @@ -1,5 +1,8 @@ import { type ReasoningUIPart, type ToolUIPart } from 'ai'; -import { type ExtendedUIMessagePart } from 'twenty-shared/ai'; +import { + type ExtendedFileUIPart, + type ExtendedUIMessagePart, +} from 'twenty-shared/ai'; import { type AgentMessagePart } from '~/generated-metadata/graphql'; export const mapDBPartToUIMessagePart = ( @@ -23,7 +26,8 @@ export const mapDBPartToUIMessagePart = ( mediaType: part.fileMediaType!, filename: part.fileFilename!, url: part.fileUrl!, - }; + fileId: part.fileId!, + } as ExtendedFileUIPart; case 'source-url': return { type: 'source-url', diff --git a/packages/twenty-front/src/modules/attachments/graphql/queries/uploadFile.ts b/packages/twenty-front/src/modules/attachments/graphql/queries/uploadFile.ts deleted file mode 100644 index 632d32a343..0000000000 --- a/packages/twenty-front/src/modules/attachments/graphql/queries/uploadFile.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from '@apollo/client'; - -export const UPLOAD_FILE = gql` - mutation uploadFile($file: Upload!, $fileFolder: FileFolder) { - uploadFile(file: $file, fileFolder: $fileFolder) { - path - token - } - } -`; diff --git a/packages/twenty-front/src/modules/attachments/graphql/queries/uploadImage.ts b/packages/twenty-front/src/modules/attachments/graphql/queries/uploadImage.ts deleted file mode 100644 index 7c86e292c9..0000000000 --- a/packages/twenty-front/src/modules/attachments/graphql/queries/uploadImage.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from '@apollo/client'; - -export const UPLOAD_IMAGE = gql` - mutation uploadImage($file: Upload!, $fileFolder: FileFolder) { - uploadImage(file: $file, fileFolder: $fileFolder) { - path - token - } - } -`; diff --git a/packages/twenty-front/src/modules/blocknote-editor/hooks/useAttachmentSync.ts b/packages/twenty-front/src/modules/blocknote-editor/hooks/useAttachmentSync.ts index 1fa8bbb037..7ee03924dc 100644 --- a/packages/twenty-front/src/modules/blocknote-editor/hooks/useAttachmentSync.ts +++ b/packages/twenty-front/src/modules/blocknote-editor/hooks/useAttachmentSync.ts @@ -8,13 +8,8 @@ import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; export const useAttachmentSync = (attachments: Attachment[]) => { - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); const { deleteManyRecords: deleteAttachments } = useDeleteManyRecords({ objectNameSingular: CoreObjectNameSingular.Attachment, }); @@ -47,7 +42,6 @@ export const useAttachmentSync = (attachments: Attachment[]) => { newBody, attachments, previousBodyOrEmptyArray, - isFilesFieldMigrated, ); if (attachmentIdsToDelete.length > 0) { @@ -59,7 +53,6 @@ export const useAttachmentSync = (attachments: Attachment[]) => { const attachmentPathsToRestore = getActivityAttachmentPathsToRestore( newBody, attachments, - isFilesFieldMigrated, ); if (attachmentPathsToRestore.length > 0) { @@ -69,7 +62,6 @@ export const useAttachmentSync = (attachments: Attachment[]) => { const attachmentIdsToRestore = filterAttachmentsToRestore({ attachmentPathsToRestore, softDeletedAttachments: softDeletedAttachments ?? [], - isFilesFieldMigrated, }); await restoreAttachments({ @@ -80,7 +72,6 @@ export const useAttachmentSync = (attachments: Attachment[]) => { const attachmentsToUpdate = getActivityAttachmentIdsAndNameToUpdate( newBody, attachments, - isFilesFieldMigrated, ); for (const attachmentToUpdate of attachmentsToUpdate) { diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx index 2b0c929e3b..cf8eb4d722 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx @@ -12,16 +12,12 @@ import { recordStoreIdentifierFamilySelector } from '@/object-record/record-stor import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell'; import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { Trans } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { Avatar } from 'twenty-ui/display'; -import { - FeatureFlagKey, - FieldMetadataType, -} from '~/generated-metadata/graphql'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout'; @@ -57,16 +53,11 @@ export const CommandMenuRecordInfo = ({ }, ) as string | null; - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); - const recordIdentifier = useAtomFamilySelectorValue( recordStoreIdentifierFamilySelector, { recordId: objectRecordId, allowRequestsToTwentyIcons, - isFilesFieldMigrated, }, ); diff --git a/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts b/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts deleted file mode 100644 index 97c13f0914..0000000000 --- a/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { gql } from '@apollo/client'; - -export const CREATE_FILE = gql` - mutation CreateFile($file: Upload!) { - createFile(file: $file) { - id - path - size - createdAt - } - } -`; diff --git a/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts b/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts deleted file mode 100644 index 72ca1e5e02..0000000000 --- a/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { gql } from '@apollo/client'; - -export const DELETE_FILE = gql` - mutation DeleteFile($fileId: UUID!) { - deleteFile(fileId: $fileId) { - id - path - size - createdAt - } - } -`; diff --git a/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx b/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx index 76e83956fa..9a59be00b3 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/PreComputedChipGeneratorsProvider.tsx @@ -5,8 +5,6 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; export const PreComputedChipGeneratorsProvider = ({ children, @@ -15,17 +13,13 @@ export const PreComputedChipGeneratorsProvider = ({ const allowRequestsToTwentyIcons = useAtomStateValue( allowRequestsToTwentyIconsState, ); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); const { chipGeneratorPerObjectPerField, identifierChipGeneratorPerObject } = useMemo(() => { return getRecordChipGenerators( objectMetadataItems, allowRequestsToTwentyIcons, - isFilesFieldMigrated, ); - }, [allowRequestsToTwentyIcons, isFilesFieldMigrated, objectMetadataItems]); + }, [allowRequestsToTwentyIcons, objectMetadataItems]); return ( <> diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts b/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts index bac71de6fd..65de69ac71 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getAvatarUrl.ts @@ -3,13 +3,7 @@ import { CoreObjectNameSingular } from 'twenty-shared/types'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isNonEmptyString } from '@sniptt/guards'; -import { - getImageAbsoluteURI, - getLogoUrlFromDomainName, - isDefined, -} from 'twenty-shared/utils'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils'; import { getImageIdentifierFieldValue } from './getImageIdentifierFieldValue'; export const getAvatarUrl = ( @@ -17,7 +11,6 @@ export const getAvatarUrl = ( record: ObjectRecord, imageIdentifierFieldMetadataItem: FieldMetadataItem | undefined, allowRequestsToTwentyIcons?: boolean | undefined, - isFilesFieldMigrated?: boolean | undefined, ) => { if (objectNameSingular === CoreObjectNameSingular.WorkspaceMember) { return record.avatarUrl ?? undefined; @@ -33,16 +26,7 @@ export const getAvatarUrl = ( } if (objectNameSingular === CoreObjectNameSingular.Person) { - if (isFilesFieldMigrated === true) { - return record.avatarFile?.[0]?.url ?? ''; - } - - return isNonEmptyString(record.avatarUrl) - ? getImageAbsoluteURI({ - imageUrl: record.avatarUrl, - baseUrl: REACT_APP_SERVER_BASE_URL, - }) - : ''; + return record.avatarFile?.[0]?.url ?? ''; } const imageIdentifierFieldValue = getImageIdentifierFieldValue( diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getImageIdentifierFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/utils/getImageIdentifierFieldMetadataItem.ts index 0bac6a7331..82a4c53093 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getImageIdentifierFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getImageIdentifierFieldMetadataItem.ts @@ -7,12 +7,10 @@ export const getImageIdentifierFieldMetadataItem = ( ObjectMetadataItem, 'fields' | 'imageIdentifierFieldMetadataId' | 'nameSingular' >, - isFilesFieldMigrated?: boolean, ): FieldMetadataItem | undefined => objectMetadataItem.fields.find((fieldMetadataItem) => isImageIdentifierField({ fieldMetadataItem, objectMetadataItem, - isFilesFieldMigrated, }), ); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts index ce45d4af80..29411b9b84 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectRecordIdentifier.ts @@ -12,7 +12,6 @@ export const getObjectRecordIdentifier = ({ objectMetadataItem, record, allowRequestsToTwentyIcons, - isFilesFieldMigrated, }: { objectMetadataItem: Pick< ObjectMetadataItem, @@ -23,7 +22,6 @@ export const getObjectRecordIdentifier = ({ >; record: ObjectRecord; allowRequestsToTwentyIcons: boolean; - isFilesFieldMigrated?: boolean; }): ObjectRecordIdentifier => { const labelIdentifierFieldMetadataItem = getLabelIdentifierFieldMetadataItem(objectMetadataItem); @@ -45,7 +43,6 @@ export const getObjectRecordIdentifier = ({ record, imageIdentifierFieldMetadata, allowRequestsToTwentyIcons, - isFilesFieldMigrated, ); const linkToShowPage = getLinkToShowPage( diff --git a/packages/twenty-front/src/modules/object-metadata/utils/isImageIdentifierField.ts b/packages/twenty-front/src/modules/object-metadata/utils/isImageIdentifierField.ts index 9839588af7..a6eddf35c9 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/isImageIdentifierField.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/isImageIdentifierField.ts @@ -5,14 +5,12 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI export const isImageIdentifierField = ({ fieldMetadataItem, objectMetadataItem, - isFilesFieldMigrated, }: { fieldMetadataItem: Pick; objectMetadataItem: Pick< ObjectMetadataItem, 'imageIdentifierFieldMetadataId' | 'nameSingular' >; - isFilesFieldMigrated?: boolean; }) => { if ( objectMetadataItem.nameSingular === CoreObjectNameSingular.Company && @@ -22,10 +20,7 @@ export const isImageIdentifierField = ({ } if (objectMetadataItem.nameSingular === CoreObjectNameSingular.Person) { - if (isFilesFieldMigrated === true) { - return fieldMetadataItem.name === 'avatarFile'; - } - return fieldMetadataItem.name === 'avatarUrl'; + return fieldMetadataItem.name === 'avatarFile'; } return ( diff --git a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/__tests__/__snapshots__/generateDepthRecordGqlFieldsFromObject.test.ts.snap b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/__tests__/__snapshots__/generateDepthRecordGqlFieldsFromObject.test.ts.snap index 54af56fee5..5b1e82a406 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/__tests__/__snapshots__/generateDepthRecordGqlFieldsFromObject.test.ts.snap +++ b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/__tests__/__snapshots__/generateDepthRecordGqlFieldsFromObject.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record gql fields from object 1`] = ` { @@ -45,7 +45,7 @@ exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record "name": true, }, "people": { - "avatarUrl": true, + "avatarFile": true, "id": true, "name": true, }, @@ -53,7 +53,7 @@ exports[`generateDepthRecordGqlFieldsFromObject should generate depth one record "previousEmployees": { "id": true, "person": { - "avatarUrl": true, + "avatarFile": true, "id": true, "name": true, }, diff --git a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/buildIdentifierGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/buildIdentifierGqlFields.ts index 41bdef5ab5..0df0513cc0 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/buildIdentifierGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/buildIdentifierGqlFields.ts @@ -12,14 +12,11 @@ export const buildIdentifierGqlFields = ( | 'imageIdentifierFieldMetadataId' | 'nameSingular' >, - isFilesFieldMigrated?: boolean, ): RecordGqlFields => { const labelIdentifierField = getLabelIdentifierFieldMetadataItem(objectMetadata); - const imageIdentifierField = getImageIdentifierFieldMetadataItem( - objectMetadata, - isFilesFieldMigrated, - ); + const imageIdentifierField = + getImageIdentifierFieldMetadataItem(objectMetadata); return { id: true, diff --git a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields.ts b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields.ts index 2947b6c0ec..d288da15ea 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields.ts @@ -30,7 +30,6 @@ export type GenerateDepthRecordGqlFieldsFromFields = { >[]; depth: 0 | 1; shouldOnlyLoadRelationIdentifiers?: boolean; - isFilesFieldMigrated?: boolean; }; export const generateDepthRecordGqlFieldsFromFields = ({ @@ -38,7 +37,6 @@ export const generateDepthRecordGqlFieldsFromFields = ({ fields, depth, shouldOnlyLoadRelationIdentifiers = true, - isFilesFieldMigrated, }: GenerateDepthRecordGqlFieldsFromFields) => { const generatedRecordGqlFields: RecordGqlFields = fields.reduce( (recordGqlFields, fieldMetadata) => { @@ -88,7 +86,6 @@ export const generateDepthRecordGqlFieldsFromFields = ({ const junctionGqlFields = generateJunctionRelationGqlFields({ fieldMetadataItem: fieldMetadata, objectMetadataItems, - isFilesFieldMigrated, }); if (isDefined(junctionGqlFields) && depth === 1) { @@ -103,10 +100,7 @@ export const generateDepthRecordGqlFieldsFromFields = ({ getLabelIdentifierFieldMetadataItem(targetObjectMetadataItem); const imageIdentifierFieldMetadataItem = - getImageIdentifierFieldMetadataItem( - targetObjectMetadataItem, - isFilesFieldMigrated, - ); + getImageIdentifierFieldMetadataItem(targetObjectMetadataItem); const relationIdentifierSubGqlFields = { id: true, diff --git a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateJunctionRelationGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateJunctionRelationGqlFields.ts index 9410a50b87..fee5a97fa3 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateJunctionRelationGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateJunctionRelationGqlFields.ts @@ -16,13 +16,11 @@ type JunctionFieldMetadataItem = Pick< type GenerateJunctionRelationGqlFieldsArgs = { fieldMetadataItem: JunctionFieldMetadataItem; objectMetadataItems: JunctionObjectMetadataItem[]; - isFilesFieldMigrated?: boolean; }; const buildRegularTargetFieldGqlFields = ( targetField: JunctionFieldMetadataItem, objectMetadataItems: JunctionObjectMetadataItem[], - isFilesFieldMigrated?: boolean, ): RecordGqlFields => { const targetObjectMetadata = objectMetadataItems.find( (item) => item.id === targetField.relation?.targetObjectMetadata.id, @@ -33,17 +31,13 @@ const buildRegularTargetFieldGqlFields = ( } return { - [targetField.name]: buildIdentifierGqlFields( - targetObjectMetadata, - isFilesFieldMigrated, - ), + [targetField.name]: buildIdentifierGqlFields(targetObjectMetadata), }; }; const buildMorphTargetFieldGqlFields = ( targetField: JunctionFieldMetadataItem, objectMetadataItems: JunctionObjectMetadataItem[], - isFilesFieldMigrated?: boolean, ): RecordGqlFields => { const morphRelations = targetField.morphRelations; @@ -69,10 +63,7 @@ const buildMorphTargetFieldGqlFields = ( targetObjectMetadataNamePlural: targetObjectMetadata.namePlural, }); - result[computedFieldName] = buildIdentifierGqlFields( - targetObjectMetadata, - isFilesFieldMigrated, - ); + result[computedFieldName] = buildIdentifierGqlFields(targetObjectMetadata); } return result; @@ -81,27 +72,17 @@ const buildMorphTargetFieldGqlFields = ( const buildTargetFieldGqlFields = ( targetField: JunctionFieldMetadataItem, objectMetadataItems: JunctionObjectMetadataItem[], - isFilesFieldMigrated?: boolean, ): RecordGqlFields => { if (targetField.type === FieldMetadataType.MORPH_RELATION) { - return buildMorphTargetFieldGqlFields( - targetField, - objectMetadataItems, - isFilesFieldMigrated, - ); + return buildMorphTargetFieldGqlFields(targetField, objectMetadataItems); } - return buildRegularTargetFieldGqlFields( - targetField, - objectMetadataItems, - isFilesFieldMigrated, - ); + return buildRegularTargetFieldGqlFields(targetField, objectMetadataItems); }; // Generates GraphQL fields for a junction relation, including the nested target objects export const generateJunctionRelationGqlFields = ({ fieldMetadataItem, objectMetadataItems, - isFilesFieldMigrated, }: GenerateJunctionRelationGqlFieldsArgs): RecordGqlFields | null => { const junctionConfig = getJunctionConfig({ settings: fieldMetadataItem.settings, @@ -119,17 +100,13 @@ export const generateJunctionRelationGqlFields = ({ const junctionTargetFields = targetFields.reduce( (acc, targetField) => ({ ...acc, - ...buildTargetFieldGqlFields( - targetField, - objectMetadataItems, - isFilesFieldMigrated, - ), + ...buildTargetFieldGqlFields(targetField, objectMetadataItems), }), {}, ); return { - ...buildIdentifierGqlFields(junctionObjectMetadata, isFilesFieldMigrated), + ...buildIdentifierGqlFields(junctionObjectMetadata), ...junctionTargetFields, }; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx index 8c4ce92c95..b4233afde8 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useRecordIndexTableQuery.test.tsx @@ -51,7 +51,12 @@ const mocks: MockedResponse[] = [ edges { node { __typename - avatarUrl + avatarFile { + fileId + label + extension + url + } createdAt deletedAt id diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts index 1ecfb2a31f..efb6ac26f9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useRecordsFieldVisibleGqlFields.ts @@ -9,9 +9,7 @@ import { generateDepthRecordGqlFieldsFromFields } from '@/object-record/graphql/ import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from 'twenty-shared/utils'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; type UseRecordsFieldVisibleGqlFields = { objectMetadataItem: ObjectMetadataItem; @@ -31,10 +29,6 @@ export const useRecordsFieldVisibleGqlFields = ({ const { objectMetadataItems } = useObjectMetadataItems(); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); - const allDepthOneGqlFields = generateDepthRecordGqlFieldsFromFields({ objectMetadataItems, fields: visibleRecordFields @@ -44,15 +38,12 @@ export const useRecordsFieldVisibleGqlFields = ({ ) .filter(isDefined), depth: 1, - isFilesFieldMigrated, }); const labelIdentifierFieldMetadataItem = getLabelIdentifierFieldMetadataItem(objectMetadataItem); - const imageIdentifierFieldMetadataItem = getImageIdentifierFieldMetadataItem( - objectMetadataItem, - isFilesFieldMigrated, - ); + const imageIdentifierFieldMetadataItem = + getImageIdentifierFieldMetadataItem(objectMetadataItem); const hasPosition = hasObjectMetadataItemPositionField(objectMetadataItem); diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx index 8d764ab5d3..efc7db9e58 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx @@ -13,14 +13,10 @@ import { RecordTitleCell } from '@/object-record/record-title-cell/components/Re import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType'; import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { isDefined } from 'twenty-shared/utils'; -import { - FieldMetadataType, - FeatureFlagKey, -} from '~/generated-metadata/graphql'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; type SummaryCardProps = { objectNameSingular: string; @@ -48,9 +44,6 @@ export const SummaryCard = ({ const allowRequestsToTwentyIcons = useAtomStateValue( allowRequestsToTwentyIconsState, ); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ objectNameSingular, @@ -65,7 +58,6 @@ export const SummaryCard = ({ { recordId: objectRecordId, allowRequestsToTwentyIcons, - isFilesFieldMigrated, }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/usePersonAvatarUpload.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/usePersonAvatarUpload.ts index 574efcbcdc..9a2b62a22c 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/usePersonAvatarUpload.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/usePersonAvatarUpload.ts @@ -1,30 +1,21 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useApolloClient } from '@apollo/client'; import { t } from '@lingui/core/macro'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { - FileFolder, useUploadFilesFieldFileMutation, - useUploadImageMutation, - FeatureFlagKey, FieldMetadataType, } from '~/generated-metadata/graphql'; export const usePersonAvatarUpload = (personRecordId: string) => { const apolloClient = useApolloClient(); - const [uploadImage] = useUploadImageMutation(); const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({ client: apolloClient, }); const { updateOneRecord } = useUpdateOneRecord(); - const isFilesFieldMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - ); - const { objectMetadataItem: personMetadata } = useObjectMetadataItem({ objectNameSingular: CoreObjectNameSingular.Person, }); @@ -35,56 +26,33 @@ export const usePersonAvatarUpload = (personRecordId: string) => { )?.id; const onUploadPicture = async (file: File) => { - if (isFilesFieldMigrated) { - assertIsDefinedOrThrow( - avatarFileFieldMetadataId, - new Error(t`Avatar file field not found for person object`), - ); + assertIsDefinedOrThrow( + avatarFileFieldMetadataId, + new Error(t`Avatar file field not found for person object`), + ); - const result = await uploadFilesFieldFile({ - variables: { file, fieldMetadataId: avatarFileFieldMetadataId }, - }); + const result = await uploadFilesFieldFile({ + variables: { file, fieldMetadataId: avatarFileFieldMetadataId }, + }); - const uploadedFile = result?.data?.uploadFilesFieldFile; + const uploadedFile = result?.data?.uploadFilesFieldFile; - if (!isDefined(uploadedFile)) { - return; - } - - await updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.Person, - idToUpdate: personRecordId, - updateOneRecordInput: { - avatarFile: [ - { - fileId: uploadedFile.id, - label: file.name, - }, - ], - }, - }); - } else { - const result = await uploadImage({ - variables: { - file, - fileFolder: FileFolder.PersonPicture, - }, - }); - - const avatarSignedFile = result?.data?.uploadImage; - - if (!avatarSignedFile) { - return; - } - - await updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.Person, - idToUpdate: personRecordId, - updateOneRecordInput: { - avatarUrl: avatarSignedFile.path, - }, - }); + if (!isDefined(uploadedFile)) { + return; } + + await updateOneRecord({ + objectNameSingular: CoreObjectNameSingular.Person, + idToUpdate: personRecordId, + updateOneRecordInput: { + avatarFile: [ + { + fileId: uploadedFile.id, + label: file.name, + }, + ], + }, + }); }; return { onUploadPicture }; diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierFamilySelector.ts index d98a99e6d6..b844b125d5 100644 --- a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreIdentifierFamilySelector.ts @@ -8,7 +8,6 @@ import { uncapitalize } from 'twenty-shared/utils'; type RecordStoreIdentifierFamilyKey = { recordId: string; allowRequestsToTwentyIcons: boolean; - isFilesFieldMigrated?: boolean; }; export const recordStoreIdentifierFamilySelector = createAtomFamilySelector< @@ -20,7 +19,6 @@ export const recordStoreIdentifierFamilySelector = createAtomFamilySelector< ({ recordId, allowRequestsToTwentyIcons, - isFilesFieldMigrated, }: RecordStoreIdentifierFamilyKey) => ({ get }) => { const recordFromStore = get(recordStoreFamilyState, recordId); @@ -42,7 +40,6 @@ export const recordStoreIdentifierFamilySelector = createAtomFamilySelector< objectMetadataItem: objectMetadataItem, record: recordFromStore, allowRequestsToTwentyIcons, - isFilesFieldMigrated, }); }, }); diff --git a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts index c83fd2e7f2..1184013180 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts @@ -17,7 +17,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; export const getRecordChipGenerators = ( objectMetadataItems: ObjectMetadataItem[], allowRequestsToTwentyIcons?: boolean, - isFilesFieldMigrated?: boolean, ) => { const chipGeneratorPerObjectPerField: ChipGeneratorPerObjectNameSingularPerFieldName = {}; @@ -95,7 +94,6 @@ export const getRecordChipGenerators = ( record, imageIdentifierFieldMetadataToUse, allowRequestsToTwentyIcons, - isFilesFieldMigrated, ), avatarType, isLabelIdentifier, diff --git a/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePictureLegacy.ts b/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePictureLegacy.ts deleted file mode 100644 index 2ead74f73e..0000000000 --- a/packages/twenty-front/src/modules/settings/members/graphql/mutations/uploadWorkspaceMemberProfilePictureLegacy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from '@apollo/client'; - -export const UPLOAD_WORKSPACE_MEMBER_PROFILE_PICTURE_LEGACY = gql` - mutation UploadWorkspaceMemberProfilePictureLegacy($file: Upload!) { - uploadWorkspaceMemberProfilePictureLegacy(file: $file) { - path - token - } - } -`; 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 1ba5fc2fa9..84c8ded1e0 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 @@ -8,13 +8,8 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useCanEditProfileField } from '@/settings/profile/hooks/useCanEditProfileField'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ImageInput } from '@/ui/input/components/ImageInput'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { buildSignedPath, isDefined } from 'twenty-shared/utils'; -import { - FeatureFlagKey, - useUploadWorkspaceMemberProfilePictureLegacyMutation, - useUploadWorkspaceMemberProfilePictureMutation, -} from '~/generated-metadata/graphql'; +import { isDefined } from 'twenty-shared/utils'; +import { useUploadWorkspaceMemberProfilePictureMutation } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type WorkspaceMemberPictureUploaderProps = { @@ -30,9 +25,6 @@ export const WorkspaceMemberPictureUploader = ({ onAvatarUpdated, disabled = false, }: WorkspaceMemberPictureUploaderProps) => { - const isCorePictureMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - ); const { enqueueErrorSnackBar } = useSnackBar(); const [isUploading, setIsUploading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); @@ -44,8 +36,6 @@ export const WorkspaceMemberPictureUploader = ({ ); const [uploadPicture] = useUploadWorkspaceMemberProfilePictureMutation(); - const [uploadPictureLegacy] = - useUploadWorkspaceMemberProfilePictureLegacyMutation(); const { updateOneRecord } = useUpdateOneRecord(); @@ -67,52 +57,28 @@ export const WorkspaceMemberPictureUploader = ({ let newAvatarUrl: string | null = null; try { - if (!isCorePictureMigrated) { - const { data } = await uploadPictureLegacy({ - variables: { file }, - context: { - fetchOptions: { - signal: controller.signal, - }, + const { data } = await uploadPicture({ + variables: { file }, + context: { + fetchOptions: { + signal: controller.signal, }, - }); + }, + }); - const signedFile = data?.uploadWorkspaceMemberProfilePictureLegacy; - if (!isDefined(signedFile)) { - throw new Error('Avatar upload failed'); - } - - await updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.WorkspaceMember, - idToUpdate: workspaceMemberId, - updateOneRecordInput: { avatarUrl: signedFile.path }, - }); - - newAvatarUrl = buildSignedPath(signedFile); - } else { - const { data } = await uploadPicture({ - variables: { file }, - context: { - fetchOptions: { - signal: controller.signal, - }, - }, - }); - - const signedFile = data?.uploadWorkspaceMemberProfilePicture; - if (!isDefined(signedFile)) { - throw new Error('Avatar upload failed'); - } - - await updateOneRecord({ - objectNameSingular: CoreObjectNameSingular.WorkspaceMember, - idToUpdate: workspaceMemberId, - updateOneRecordInput: { avatarUrl: signedFile.url }, - }); - - newAvatarUrl = signedFile.url; + const signedFile = data?.uploadWorkspaceMemberProfilePicture; + if (!isDefined(signedFile)) { + throw new Error('Avatar upload failed'); } + await updateOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + idToUpdate: workspaceMemberId, + updateOneRecordInput: { avatarUrl: signedFile.url }, + }); + + newAvatarUrl = signedFile.url; + if (isEditingSelf && isDefined(currentWorkspaceMember)) { setCurrentWorkspaceMember({ ...currentWorkspaceMember, diff --git a/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx b/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx index ca77bcace8..1d0115dd87 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx @@ -1,21 +1,13 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { ImageInput } from '@/ui/input/components/ImageInput'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { buildSignedPath } from 'twenty-shared/utils'; import { - FeatureFlagKey, useUpdateWorkspaceMutation, - useUploadWorkspaceLogoLegacyMutation, useUploadWorkspaceLogoMutation, } from '~/generated-metadata/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const WorkspaceLogoUploader = () => { - const isCorePictureMigrated = useIsFeatureEnabled( - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - ); - const [uploadLogoLegacy] = useUploadWorkspaceLogoLegacyMutation(); const [uploadLogo] = useUploadWorkspaceLogoMutation(); const [updateWorkspace] = useUpdateWorkspaceMutation(); const [currentWorkspace, setCurrentWorkspace] = useAtomState( @@ -30,31 +22,17 @@ export const WorkspaceLogoUploader = () => { throw new Error('Workspace id not found'); } - if (isCorePictureMigrated) { - await uploadLogo({ - variables: { - file, - }, - onCompleted: (data) => { - setCurrentWorkspace({ - ...currentWorkspace, - logo: data.uploadWorkspaceLogo.url, - }); - }, - }); - } else { - await uploadLogoLegacy({ - variables: { - file, - }, - onCompleted: (data) => { - setCurrentWorkspace({ - ...currentWorkspace, - logo: buildSignedPath(data.uploadWorkspaceLogoLegacy), - }); - }, - }); - } + await uploadLogo({ + variables: { + file, + }, + onCompleted: (data) => { + setCurrentWorkspace({ + ...currentWorkspace, + logo: data.uploadWorkspaceLogo.url, + }); + }, + }); }; const onRemove = async () => { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx index 7eac26829a..f9aa9c7152 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmailBase.tsx @@ -1,20 +1,13 @@ import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { getMissingDraftEmailScopes } from '@/accounts/utils/hasMissingDraftEmailScopes'; -import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { WorkflowSendEmailAttachments } from '@/advanced-text-editor/components/WorkflowSendEmailAttachments'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { - CoreObjectNameSingular, - ConnectedAccountProvider, - SettingsPath, -} from 'twenty-shared/types'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput'; import { FormMultiTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiTextFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput'; import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -22,17 +15,18 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; +import { type WorkflowEmailAction } from '@/workflow/types/WorkflowEmailAction'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; import { useEmailForm } from '@/workflow/workflow-steps/workflow-actions/hooks/useEmailForm'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { t } from '@lingui/core/macro'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useEffect, useState } from 'react'; +import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { type WorkflowEmailAction } from '@/workflow/types/WorkflowEmailAction'; import { Callout, IconPlus } from 'twenty-ui/display'; import { Button, type SelectOption } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; @@ -60,8 +54,6 @@ export const WorkflowEditActionEmailBase = ({ }: WorkflowEditActionEmailBaseProps) => { const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); const { triggerApisOAuth } = useTriggerApisOAuth(); - const { enqueueErrorSnackBar } = useSnackBar(); - const { uploadAttachmentFile } = useUploadAttachmentFile(); const workflowVisualizerWorkflowId = useAtomComponentStateValue( workflowVisualizerWorkflowIdComponentState, @@ -114,25 +106,6 @@ export const WorkflowEditActionEmailBase = ({ handleFieldChange('connectedAccountId', connectedAccountId); }; - const handleUploadAttachment = async (file: File) => { - if (!isDefined(workflowVisualizerWorkflowId)) { - return undefined; - } - - const { attachmentAbsoluteURL } = await uploadAttachmentFile(file, { - id: workflowVisualizerWorkflowId, - targetObjectNameSingular: CoreObjectNameSingular.Workflow, - }); - - return attachmentAbsoluteURL; - }; - - const handleImageUploadError = (_: Error, file: File) => { - enqueueErrorSnackBar({ - message: t`Failed to upload image: `.concat(file.name), - }); - }; - const filter: { or: object[] } = { or: [ { @@ -380,8 +353,6 @@ export const WorkflowEditActionEmailBase = ({ children: t`Email Editor`, }, ]} - onImageUpload={handleUploadAttachment} - onImageUploadError={handleImageUploadError} minHeight={EMAIL_EDITOR_MIN_HEIGHT} maxWidth={EMAIL_EDITOR_MAX_WIDTH} /> diff --git a/packages/twenty-front/src/modules/workspace/graphql/mutations/uploadWorkspaceLogoLegacy.ts b/packages/twenty-front/src/modules/workspace/graphql/mutations/uploadWorkspaceLogoLegacy.ts deleted file mode 100644 index bfcad00089..0000000000 --- a/packages/twenty-front/src/modules/workspace/graphql/mutations/uploadWorkspaceLogoLegacy.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from '@apollo/client'; - -export const UPLOAD_WORKSPACE_LOGO_LEGACY = gql` - mutation UploadWorkspaceLogoLegacy($file: Upload!) { - uploadWorkspaceLogoLegacy(file: $file) { - path - token - } - } -`; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-seed-workflow-v1-16.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-seed-workflow-v1-16.command.ts deleted file mode 100644 index 05566de6de..0000000000 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-seed-workflow-v1-16.command.ts +++ /dev/null @@ -1,531 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Command } from 'nest-commander'; -import { Repository } from 'typeorm'; -import { v4 as uuidv4 } from 'uuid'; - -import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; -import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { - LogicFunctionEntity, - LogicFunctionRuntime, -} from 'src/engine/metadata-modules/logic-function/logic-function.entity'; -import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; -import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; -import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum'; -import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; -import { - DEFAULT_BUILT_HANDLER_PATH, - DEFAULT_HANDLER_NAME, - DEFAULT_SOURCE_HANDLER_PATH, -} from 'src/engine/metadata-modules/logic-function/constants/handler.contant'; - -const OLD_BUILT_FOLDER = 'built-function'; -const OLD_SOURCE_FOLDER = 'serverless-function'; -const SEED_VERSION_DRAFT = 'draft'; -const SEED_VERSION_PUBLISHED = '1'; - -const OUTPUT_SCHEMA_LINK = { - link: { - tab: 'test', - icon: 'IconVariable', - label: 'Generate Function Output', - isLeaf: true, - }, - _outputSchemaType: 'LINK', -} as const; - -const OUTPUT_SCHEMA_MESSAGE = { - message: { - type: 'string', - label: 'message', - value: 'Hello, input: null and null', - isLeaf: true, - }, -} as const; - -@Command({ - name: 'upgrade:1-17:seed-workflow-v1-16', - description: - '[Temporary] Clean existing workflow runs, workflow versions, workflows, logic functions and old file storage, then seed 3 scenarios: (1) draft+active with LINK outputSchema, (2) draft-only with message outputSchema, (3) draft+active with mixed outputSchema (message on draft, LINK on active). For testing the 1-17 migrate-workflow-code-steps command.', -}) -export class SeedWorkflowV1_16Command extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { - protected readonly logger = new Logger(SeedWorkflowV1_16Command.name); - - constructor( - @InjectRepository(WorkspaceEntity) - protected readonly workspaceRepository: Repository, - @InjectRepository(LogicFunctionEntity) - private readonly logicFunctionRepository: Repository, - protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - protected readonly dataSourceService: DataSourceService, - private readonly applicationService: ApplicationService, - private readonly fileStorageService: FileStorageService, - private readonly recordPositionService: RecordPositionService, - ) { - super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); - } - - override async runOnWorkspace({ - workspaceId, - }: RunOnWorkspaceArgs): Promise { - this.logger.log(`Seeding workflow v1.16 data for workspace ${workspaceId}`); - - await this.cleanWorkflowsAndOldFileStorage(workspaceId); - - const workflowRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflow', - { - shouldBypassPermissionChecks: true, - }, - ); - const workflowVersionRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - - await this.seedScenarioDraftAndActiveLink( - workspaceId, - workflowRepository, - workflowVersionRepository, - ); - await this.seedScenarioDraftOnlyMessage( - workspaceId, - workflowRepository, - workflowVersionRepository, - ); - await this.seedScenarioDraftAndActiveMixedOutputSchema( - workspaceId, - workflowRepository, - workflowVersionRepository, - ); - - this.logger.log( - `Seeded 3 workflows (draft+active LINK, draft-only message, draft+active mixed outputSchema) in workspace ${workspaceId}. Run upgrade:1-17:migrate-workflow-code-steps to test migration.`, - ); - } - - private buildCodeStep( - logicFunctionId: string, - version: string, - outputSchema: object, - stepName: string, - ) { - return { - id: uuidv4(), - name: stepName, - type: WorkflowActionType.CODE, - settings: { - input: { - serverlessFunctionId: logicFunctionId, - serverlessFunctionInput: {}, - serverlessFunctionVersion: version, - }, - outputSchema, - }, - valid: true, - }; - } - - private buildTrigger(): { - name: string; - type: string; - settings: { outputSchema: object }; - } { - return { - name: 'trigger', - type: WorkflowTriggerType.MANUAL, - settings: { outputSchema: {} }, - }; - } - - private async seedScenarioDraftAndActiveLink( - workspaceId: string, - workflowRepository: Awaited< - ReturnType - >, - workflowVersionRepository: Awaited< - ReturnType - >, - ): Promise { - const logicFunctionId = await this.insertLogicFunctionRow( - workspaceId, - 'Seed (draft+active LINK)', - ); - - await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_DRAFT); - await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_PUBLISHED); - - const workflowId = uuidv4(); - const draftVersionId = uuidv4(); - const activeVersionId = uuidv4(); - - const workflowPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflow', - }, - workspaceId, - }); - const draftVersionPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - const activeVersionPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'last', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - - await workflowRepository.insert({ - id: workflowId, - name: 'Seed draft+active (LINK)', - statuses: [WorkflowStatus.DRAFT], - position: workflowPosition, - }); - - const trigger = this.buildTrigger(); - const draftSteps = [ - this.buildCodeStep( - logicFunctionId, - SEED_VERSION_DRAFT, - OUTPUT_SCHEMA_LINK, - 'Code step (draft)', - ), - ]; - const activeSteps = [ - this.buildCodeStep( - logicFunctionId, - SEED_VERSION_PUBLISHED, - OUTPUT_SCHEMA_LINK, - 'Code step (v1)', - ), - ]; - - await workflowVersionRepository.insert({ - id: draftVersionId, - workflowId, - name: 'v1', - status: WorkflowVersionStatus.DRAFT, - trigger, - steps: draftSteps, - position: draftVersionPosition, - }); - await workflowVersionRepository.insert({ - id: activeVersionId, - workflowId, - name: 'v2', - status: WorkflowVersionStatus.ACTIVE, - trigger, - steps: activeSteps, - position: activeVersionPosition, - }); - await workflowRepository.update(workflowId, { - lastPublishedVersionId: activeVersionId, - statuses: [WorkflowStatus.ACTIVE], - }); - } - - private async seedScenarioDraftOnlyMessage( - workspaceId: string, - workflowRepository: Awaited< - ReturnType - >, - workflowVersionRepository: Awaited< - ReturnType - >, - ): Promise { - const logicFunctionId = await this.insertLogicFunctionRow( - workspaceId, - 'Seed (draft-only message)', - ); - - await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_DRAFT); - - const workflowId = uuidv4(); - const draftVersionId = uuidv4(); - - const workflowPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflow', - }, - workspaceId, - }); - const draftVersionPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - - await workflowRepository.insert({ - id: workflowId, - name: 'Seed draft-only (message)', - statuses: [WorkflowStatus.DRAFT], - position: workflowPosition, - }); - - const trigger = this.buildTrigger(); - const draftSteps = [ - this.buildCodeStep( - logicFunctionId, - SEED_VERSION_DRAFT, - OUTPUT_SCHEMA_MESSAGE, - 'Code step (draft)', - ), - ]; - - await workflowVersionRepository.insert({ - id: draftVersionId, - workflowId, - name: 'v1', - status: WorkflowVersionStatus.DRAFT, - trigger, - steps: draftSteps, - position: draftVersionPosition, - }); - } - - private async seedScenarioDraftAndActiveMixedOutputSchema( - workspaceId: string, - workflowRepository: Awaited< - ReturnType - >, - workflowVersionRepository: Awaited< - ReturnType - >, - ): Promise { - const logicFunctionId = await this.insertLogicFunctionRow( - workspaceId, - 'Seed (draft+active mixed)', - ); - - await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_DRAFT); - await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_PUBLISHED); - - const workflowId = uuidv4(); - const draftVersionId = uuidv4(); - const activeVersionId = uuidv4(); - - const workflowPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflow', - }, - workspaceId, - }); - const draftVersionPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - const activeVersionPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'last', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - - await workflowRepository.insert({ - id: workflowId, - name: 'Seed draft+active (mixed)', - statuses: [WorkflowStatus.DRAFT], - position: workflowPosition, - }); - - const trigger = this.buildTrigger(); - const draftSteps = [ - this.buildCodeStep( - logicFunctionId, - SEED_VERSION_DRAFT, - OUTPUT_SCHEMA_MESSAGE, - 'Code step (draft, message)', - ), - ]; - const activeSteps = [ - this.buildCodeStep( - logicFunctionId, - SEED_VERSION_PUBLISHED, - OUTPUT_SCHEMA_LINK, - 'Code step (v1, LINK)', - ), - ]; - - await workflowVersionRepository.insert({ - id: draftVersionId, - workflowId, - name: 'v1', - status: WorkflowVersionStatus.DRAFT, - trigger, - steps: draftSteps, - position: draftVersionPosition, - }); - await workflowVersionRepository.insert({ - id: activeVersionId, - workflowId, - name: 'v2', - status: WorkflowVersionStatus.ACTIVE, - trigger, - steps: activeSteps, - position: activeVersionPosition, - }); - await workflowRepository.update(workflowId, { - lastPublishedVersionId: activeVersionId, - statuses: [WorkflowStatus.ACTIVE], - }); - } - - private async insertLogicFunctionRow( - workspaceId: string, - name: string = 'Seed code step (v1.16)', - ): Promise { - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { workspaceId }, - ); - const applicationId = workspaceCustomFlatApplication.id; - const id = uuidv4(); - const universalIdentifier = uuidv4(); - const now = new Date(); - - await this.logicFunctionRepository.insert({ - id, - workspaceId, - universalIdentifier, - applicationId, - name, - description: 'Temporary logic function for 1.17 migration testing', - sourceHandlerPath: DEFAULT_SOURCE_HANDLER_PATH, - builtHandlerPath: DEFAULT_BUILT_HANDLER_PATH, - handlerName: DEFAULT_HANDLER_NAME, - runtime: LogicFunctionRuntime.NODE22, - timeoutSeconds: 300, - checksum: null, - toolInputSchema: null, - isTool: false, - cronTriggerSettings: null, - databaseEventTriggerSettings: null, - httpRouteTriggerSettings: null, - createdAt: now, - updatedAt: now, - deletedAt: null, - }); - - return id; - } - - private async cleanWorkflowsAndOldFileStorage( - workspaceId: string, - ): Promise { - const workflowRunRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflowRun', - { shouldBypassPermissionChecks: true }, - ); - const workflowVersionRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - const workflowRepository = - await this.globalWorkspaceOrmManager.getRepository( - workspaceId, - 'workflow', - { - shouldBypassPermissionChecks: true, - }, - ); - - const deletedRuns = await workflowRunRepository.delete({}); - const deletedVersions = await workflowVersionRepository.delete({}); - const deletedWorkflows = await workflowRepository.delete({}); - - const deletedLogicFunctions = await this.logicFunctionRepository.delete({ - workspaceId, - }); - - this.logger.log( - `Cleaned workspace ${workspaceId}: ${deletedRuns.affected ?? 0} workflow run(s), ${deletedVersions.affected ?? 0} workflow version(s), ${deletedWorkflows.affected ?? 0} workflow(s), ${deletedLogicFunctions.affected ?? 0} logic function(s)`, - ); - - try { - await this.fileStorageService.deleteLegacy({ - folderPath: OLD_BUILT_FOLDER, - }); - await this.fileStorageService.deleteLegacy({ - folderPath: OLD_SOURCE_FOLDER, - }); - this.logger.log( - `Cleaned old file storage: ${OLD_BUILT_FOLDER}, ${OLD_SOURCE_FOLDER}`, - ); - } catch (error) { - this.logger.warn( - `Old file storage cleanup skipped (folders may not exist): ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - private async writeOldFormatFiles( - logicFunctionId: string, - version: string, - ): Promise { - const builtFolder = `${OLD_BUILT_FOLDER}/${logicFunctionId}/${version}`; - const sourceFolder = `${OLD_SOURCE_FOLDER}/${logicFunctionId}/${version}`; - - const builtSources = { - 'index.mjs': - 'export default async function main() { return { message: "ok" }; }\n', - }; - const sourceSources = { - src: { - 'index.ts': - 'export default async function main(): Promise<{ message: string }> {\n return { message: "ok" };\n}\n', - }, - }; - - await this.fileStorageService.writeFolderLegacy(builtSources, builtFolder); - await this.fileStorageService.writeFolderLegacy( - sourceSources, - sourceFolder, - ); - } -} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts index ff0678f022..27e7954bcc 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-17/1-17-upgrade-version-command.module.ts @@ -11,7 +11,6 @@ import { MigrateDateTimeIsFilterValuesCommand } from 'src/database/commands/upgr import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command'; import { MigrateSendEmailRecipientsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command'; import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command'; -import { SeedWorkflowV1_16Command } from 'src/database/commands/upgrade-version-command/1-17/1-17-seed-workflow-v1-16.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @@ -76,7 +75,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas DeleteFileRecordsAndUpdateTableCommand, MigrateSendEmailRecipientsCommand, MigrateDateTimeIsFilterValuesCommand, - SeedWorkflowV1_16Command, BackfillApplicationPackageFilesCommand, ], exports: [ @@ -89,7 +87,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas MigrateSendEmailRecipientsCommand, MigrateDateTimeIsFilterValuesCommand, DeleteFileRecordsAndUpdateTableCommand, - SeedWorkflowV1_16Command, BackfillApplicationPackageFilesCommand, ], }) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts index 77d1a7496f..f7682805aa 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts @@ -3,7 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { isNonEmptyString } from '@sniptt/guards'; import { Command } from 'nest-commander'; import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { extractFolderPathFilenameAndTypeOrThrow, isDefined, @@ -14,7 +14,6 @@ import { v4 } from 'uuid'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; @@ -36,7 +35,7 @@ type RichTextBlock = Record; @Command({ name: 'upgrade:1-18:migrate-activity-rich-text-attachment-file-ids', description: - 'Migrate activity rich text blocks to include attachmentFileId from attachment.file field', + '[DEPRECATED] Migrate activity rich text blocks - this migration is now complete and no longer needed', }) export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { constructor( @@ -44,7 +43,6 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus protected readonly workspaceRepository: Repository, protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, - private readonly featureFlagService: FeatureFlagService, private readonly fileStorageService: FileStorageService, private readonly workspaceCacheService: WorkspaceCacheService, private readonly applicationService: ApplicationService, @@ -57,23 +55,20 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus override async runOnWorkspace({ workspaceId, - options, }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - const isFilesFieldMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - workspaceId, + this.logger.log( + `[DEPRECATED] Activity rich text attachment file IDs migration is no longer needed for workspace ${workspaceId}. ` + + `The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`, ); + } - if (isFilesFieldMigrated) { - this.logger.log( - `Files field already migrated for workspace ${workspaceId}, skipping`, - ); - - return; - } - + private _deprecatedMigrationLogic = async ({ + workspaceId, + isDryRun, + }: { + workspaceId: string; + isDryRun: boolean; + }) => { this.logger.log( `${isDryRun ? '[DRY RUN] ' : ''}Starting activity rich text attachment file IDs migration for workspace ${workspaceId}`, ); @@ -171,18 +166,11 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus isDryRun, }); - if (!isDryRun) { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_FILES_FIELD_MIGRATED], - workspaceId, - ); - } - this.logger.log( `${isDryRun ? '[DRY RUN] ' : ''}Completed activity rich text attachment file IDs migration for workspace ${workspaceId}`, ); }, systemAuthContext); - } + }; private async migrateActivityTable({ activityRepository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts index 757a4a7c3a..471fec43a0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command.ts @@ -3,11 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { isNonEmptyString } from '@sniptt/guards'; import { Command } from 'nest-commander'; import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { - FieldMetadataType, - FileFolder, - FeatureFlagKey, -} from 'twenty-shared/types'; +import { FieldMetadataType, FileFolder } from 'twenty-shared/types'; import { extractFolderPathFilenameAndTypeOrThrow, isDefined, @@ -19,7 +15,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -35,7 +30,7 @@ import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objec @Command({ name: 'upgrade:1-18:migrate-attachment-files', description: - 'Migrate attachment files to file field: copy files and create file records', + '[DEPRECATED] Migrate attachment files to file field - this migration is now complete and no longer needed', }) export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { constructor( @@ -43,7 +38,6 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi protected readonly workspaceRepository: Repository, protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, - private readonly featureFlagService: FeatureFlagService, private readonly fileStorageService: FileStorageService, private readonly workspaceCacheService: WorkspaceCacheService, private readonly fieldMetadataService: FieldMetadataService, @@ -56,23 +50,20 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi override async runOnWorkspace({ workspaceId, - options, }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - const isMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - workspaceId, + this.logger.log( + `[DEPRECATED] Attachment files migration is no longer needed for workspace ${workspaceId}. ` + + `The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`, ); + } - if (isMigrated) { - this.logger.log( - `Attachment files migration already completed for workspace ${workspaceId}, skipping`, - ); - - return; - } - + private _deprecatedMigrationLogic = async ({ + workspaceId, + isDryRun, + }: { + workspaceId: string; + isDryRun: boolean; + }) => { this.logger.log( `${isDryRun ? '[DRY RUN] ' : ''}Starting attachment files migration for workspace ${workspaceId}`, ); @@ -302,5 +293,5 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi `${isDryRun ? '[DRY RUN] ' : ''}Completed attachment files migration for workspace ${workspaceId}`, ); }, systemAuthContext); - } + }; } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts index 8c51b89646..c4c7cb803f 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command.ts @@ -3,11 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { isNonEmptyString } from '@sniptt/guards'; import { Command } from 'nest-commander'; import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { - FieldMetadataType, - FileFolder, - FeatureFlagKey, -} from 'twenty-shared/types'; +import { FieldMetadataType, FileFolder } from 'twenty-shared/types'; import { assertIsDefinedOrThrow, extractFolderPathFilenameAndTypeOrThrow, @@ -20,7 +16,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -36,7 +31,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso @Command({ name: 'upgrade:1-18:migrate-person-avatar-files', description: - 'Migrate person avatarUrl files to file field: copy files and create file records', + '[DEPRECATED] Migrate person avatarUrl files to file field - this migration is now complete and no longer needed', }) export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { constructor( @@ -44,7 +39,6 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces protected readonly workspaceRepository: Repository, protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, - private readonly featureFlagService: FeatureFlagService, private readonly fileStorageService: FileStorageService, private readonly workspaceCacheService: WorkspaceCacheService, private readonly fieldMetadataService: FieldMetadataService, @@ -57,23 +51,20 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces override async runOnWorkspace({ workspaceId, - options, }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - const isMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - workspaceId, + this.logger.log( + `[DEPRECATED] Person avatar files migration is no longer needed for workspace ${workspaceId}. ` + + `The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`, ); + } - if (isMigrated) { - this.logger.log( - `Person avatar files migration already completed for workspace ${workspaceId}, skipping`, - ); - - return; - } - + private _deprecatedMigrationLogic = async ({ + workspaceId, + isDryRun, + }: { + workspaceId: string; + isDryRun: boolean; + }) => { this.logger.log( `${ isDryRun ? '[DRY RUN] ' : '' @@ -304,5 +295,5 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces `${isDryRun ? '[DRY RUN] ' : ''}Completed person avatar files migration for workspace ${workspaceId}`, ); }, systemAuthContext); - } + }; } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts index d3433b9837..bc1e352b5d 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command.ts @@ -3,14 +3,13 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { isNonEmptyString } from '@sniptt/guards'; import { Command } from 'nest-commander'; -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { DataSource, In, Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -47,7 +46,7 @@ type SendEmailStep = { @Command({ name: 'upgrade:1-18:migrate-workflow-send-email-attachments', description: - 'Migrate workflow send email attachments to FileFolder.Workflow and update payload paths', + '[DEPRECATED] Migrate workflow send email attachments - this migration is now complete and no longer needed', }) export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { protected readonly logger = new Logger( @@ -59,7 +58,6 @@ export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspende protected readonly workspaceRepository: Repository, protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, - private readonly featureFlagService: FeatureFlagService, private readonly fileStorageService: FileStorageService, private readonly applicationService: ApplicationService, @InjectDataSource() @@ -70,14 +68,21 @@ export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspende override async runOnWorkspace({ workspaceId, - options, }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - const isMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_OTHER_FILE_MIGRATED, - workspaceId, + this.logger.log( + `[DEPRECATED] Workflow send email attachments migration is no longer needed for workspace ${workspaceId}. ` + + `The IS_OTHER_FILE_MIGRATED feature flag has been removed as all workspaces are now migrated.`, ); + } + + private _deprecatedMigrationLogic = async ({ + workspaceId, + isDryRun, + }: { + workspaceId: string; + isDryRun: boolean; + }) => { + const isMigrated = false; if (isMigrated) { this.logger.log( @@ -199,15 +204,8 @@ export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspende } } - if (!isDryRun) { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_OTHER_FILE_MIGRATED], - workspaceId, - ); - } - this.logger.log( `${isDryRun ? '[DRY RUN] ' : ''}Completed workflow send email attachments migration for workspace ${workspaceId}`, ); - } + }; } diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts index ad6db64692..1e80f10be8 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command.ts @@ -4,7 +4,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import FileType from 'file-type'; import { Command } from 'nest-commander'; import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { extractFolderPathFilenameAndTypeOrThrow, isDefined, @@ -16,7 +16,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; @@ -33,7 +32,7 @@ import { getImageBufferFromUrl } from 'src/utils/image'; @Command({ name: 'upgrade:1-18:migrate-workspace-pictures', description: - 'Migrate workspace logos and workspace member avatars to file records', + '[DEPRECATED] Migrate workspace logos and workspace member avatars to file records - this migration is now complete and no longer needed', }) export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { constructor( @@ -41,7 +40,6 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces protected readonly workspaceRepository: Repository, protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, - private readonly featureFlagService: FeatureFlagService, private readonly fileStorageService: FileStorageService, private readonly workspaceCacheService: WorkspaceCacheService, private readonly applicationService: ApplicationService, @@ -55,59 +53,10 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces override async runOnWorkspace({ workspaceId, - options, }: RunOnWorkspaceArgs): Promise { - const isDryRun = options.dryRun ?? false; - - const isMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - workspaceId, - ); - - if (isMigrated) { - this.logger.log( - `Workspace pictures migration already completed for workspace ${workspaceId}, skipping`, - ); - - return; - } - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Starting workspace pictures migration for workspace ${workspaceId}`, - ); - - const { workspaceCustomFlatApplication } = - await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( - { - workspaceId, - }, - ); - - const fileRepository = this.coreDataSource.getRepository(FileEntity); - - await this.migrateWorkspaceLogo({ - workspaceId, - isDryRun, - workspaceCustomFlatApplication, - fileRepository, - }); - - await this.migrateWorkspaceMemberAvatars({ - workspaceId, - isDryRun, - workspaceCustomFlatApplication, - fileRepository, - }); - - if (!isDryRun) { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_CORE_PICTURE_MIGRATED], - workspaceId, - ); - } - - this.logger.log( - `${isDryRun ? '[DRY RUN] ' : ''}Completed workspace pictures migration for workspace ${workspaceId}`, + `[DEPRECATED] Workspace pictures migration is no longer needed for workspace ${workspaceId}. ` + + `The IS_CORE_PICTURE_MIGRATED feature flag has been removed as all workspaces are now migrated.`, ); } @@ -426,12 +375,5 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces throw error; } } - - if (!isDryRun) { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_CORE_PICTURE_MIGRATED], - workspaceId, - ); - } } } diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1772555830171-replace-file-url-with-file-relation-in-agent-message-part.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1772555830171-replace-file-url-with-file-relation-in-agent-message-part.ts new file mode 100644 index 0000000000..419b73b5a5 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1772555830171-replace-file-url-with-file-relation-in-agent-message-part.ts @@ -0,0 +1,37 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class ReplaceFileUrlWithFileRelationInAgentMessagePart1772555830171 + implements MigrationInterface +{ + name = 'ReplaceFileUrlWithFileRelationInAgentMessagePart1772555830171'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" DROP COLUMN "fileUrl"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" DROP COLUMN "fileMediaType"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" ADD "fileId" uuid`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" ADD CONSTRAINT "FK_f3865544cee5742b5f5dd7340ef" FOREIGN KEY ("fileId") REFERENCES "core"."file"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" DROP CONSTRAINT "FK_f3865544cee5742b5f5dd7340ef"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" DROP COLUMN "fileId"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" ADD "fileMediaType" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "core"."agentMessagePart" ADD "fileUrl" character varying`, + ); + } +} diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts index d0f22c019f..d1785b4d5f 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts @@ -15,7 +15,6 @@ import { RichTextV2FieldQueryResultGetterHandler } from 'src/engine/api/common/c import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler'; import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -43,7 +42,6 @@ export class CommonResultGettersService { constructor( private readonly fileService: FileService, private readonly fileUrlService: FileUrlService, - private readonly featureFlagService: FeatureFlagService, ) { this.initializeObjectHandlers(); this.initializeFieldHandlers(); @@ -55,11 +53,7 @@ export class CommonResultGettersService { ['person', new PersonQueryResultGetterHandler(this.fileService)], [ 'workspaceMember', - new WorkspaceMemberQueryResultGetterHandler( - this.fileService, - this.featureFlagService, - this.fileUrlService, - ), + new WorkspaceMemberQueryResultGetterHandler(this.fileUrlService), ], ]); } @@ -75,11 +69,7 @@ export class CommonResultGettersService { ], [ FieldMetadataType.RICH_TEXT_V2, - new RichTextV2FieldQueryResultGetterHandler( - this.fileService, - this.fileUrlService, - this.featureFlagService, - ), + new RichTextV2FieldQueryResultGetterHandler(this.fileUrlService), ], ]); } diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts index 3c740026eb..f7b29e584b 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts @@ -1,9 +1,7 @@ import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; import { RichTextV2FieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler'; -import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { type FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; -import { type FileService } from 'src/engine/core-modules/file/services/file.service'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; const baseRecord: ObjectRecord = { @@ -20,28 +18,17 @@ const richTextFieldMetadata = [ }, ] as FlatFieldMetadata[]; -const mockFileService = { - signFileUrl: jest.fn().mockReturnValue('signed-path'), -} as unknown as FileService; - const mockFileUrlService = { signFileUrl: jest.fn().mockReturnValue('signed-path'), + signFileByIdUrl: jest.fn().mockReturnValue('signed-path'), } as unknown as FileUrlService; -const mockFeatureFlagService = { - isFeatureEnabled: jest.fn().mockReturnValue(true), -} as unknown as FeatureFlagService; - describe('RichTextV2FieldQueryResultGetterHandler', () => { let handler: RichTextV2FieldQueryResultGetterHandler; beforeEach(() => { process.env.SERVER_URL = 'https://my-domain.twenty.com'; - handler = new RichTextV2FieldQueryResultGetterHandler( - mockFileService, - mockFileUrlService, - mockFeatureFlagService, - ); + handler = new RichTextV2FieldQueryResultGetterHandler(mockFileUrlService); }); afterEach(() => { @@ -169,54 +156,6 @@ describe('RichTextV2FieldQueryResultGetterHandler', () => { }); }); - describe('should sign internal image URLs', () => { - it('when image block has an internal attachment URL (legacy path)', async () => { - jest - .spyOn(mockFeatureFlagService, 'isFeatureEnabled') - .mockResolvedValue(false); - - const imageBlock = { - type: 'image', - props: { - name: 'photo.jpg', - url: 'https://my-domain.twenty.com/files/attachment/some-token/photo.jpg', - caption: '', - }, - children: [], - }; - - const record = { - ...baseRecord, - bodyV2: { - markdown: null, - blocknote: JSON.stringify([imageBlock]), - }, - }; - - const result = await handler.handle( - record, - 'ws-1', - richTextFieldMetadata, - ); - - expect(result).toEqual({ - ...baseRecord, - bodyV2: { - markdown: null, - blocknote: JSON.stringify([ - { - ...imageBlock, - props: { - ...imageBlock.props, - url: 'https://my-domain.twenty.com/files/signed-path', - }, - }, - ]), - }, - }); - }); - }); - describe('should handle multiple RICH_TEXT_V2 fields', () => { it('when record has multiple rich text fields', async () => { const multiFieldMetadata = [ diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler.ts index 4a9944f3da..04a1b17f00 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler.ts @@ -1,5 +1,4 @@ import { - FeatureFlagKey, FieldMetadataType, FileFolder, type ObjectRecord, @@ -8,10 +7,8 @@ import { isDefined } from 'twenty-shared/utils'; import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; -import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { type FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util'; -import { type FileService } from 'src/engine/core-modules/file/services/file.service'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -36,11 +33,7 @@ const parseBlocknoteJsonSafely = ( export class RichTextV2FieldQueryResultGetterHandler implements QueryResultGetterHandlerInterface { - constructor( - private readonly fileService: FileService, - private readonly fileUrlService: FileUrlService, - private readonly featureFlagService: FeatureFlagService, - ) {} + constructor(private readonly fileUrlService: FileUrlService) {} async handle( record: ObjectRecord, @@ -55,11 +48,6 @@ export class RichTextV2FieldQueryResultGetterHandler return record; } - const isFilesFieldMigrated = await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - workspaceId, - ); - for (const field of richTextV2Fields) { const fieldValue = record[field.name]; const blocknoteJson = fieldValue?.blocknote; @@ -77,7 +65,6 @@ export class RichTextV2FieldQueryResultGetterHandler const signedBlocks = this.signBlocknoteImageUrls( blocknoteBlocks, workspaceId, - isFilesFieldMigrated, ); record[field.name] = { @@ -92,69 +79,32 @@ export class RichTextV2FieldQueryResultGetterHandler signBlocknoteImageUrls = ( blocknoteBlocks: RichTextBlock[], workspaceId: string, - isFilesFieldMigrated: boolean, ): RichTextBlock[] => { return blocknoteBlocks.map((block: RichTextBlock) => { - if (isFilesFieldMigrated && isDefined(block.props?.url)) { - const fileIdFromUrl = extractFileIdFromUrl( - block.props.url, - FileFolder.FilesField, - ); - - if (!isDefined(fileIdFromUrl)) { - return block; - } - - const url = this.fileUrlService.signFileByIdUrl({ - fileId: fileIdFromUrl, - workspaceId, - fileFolder: FileFolder.FilesField, - }); - - return { - ...block, - props: { - ...block.props, - url, - }, - }; - } - - if (block.type !== 'image' || !block.props?.url) { + if (!isDefined(block.props?.url)) { return block; } - let url: URL; + const fileIdFromUrl = extractFileIdFromUrl( + block.props.url, + FileFolder.FilesField, + ); - try { - url = new URL(block.props.url); - } catch { + if (!isDefined(fileIdFromUrl)) { return block; } - const pathname = url.pathname; - const isLinkExternal = !pathname.startsWith('/files/attachment/'); - - if (isLinkExternal) { - return block; - } - - const fileName = pathname.match(/files\/attachment\/(?:.+)\/(.+)$/)?.[1]; - - if (!isDefined(fileName)) { - return block; - } - - const signedPath = this.fileService.signFileUrl({ - url: `attachment/${fileName}`, + const url = this.fileUrlService.signFileByIdUrl({ + fileId: fileIdFromUrl, workspaceId, + fileFolder: FileFolder.FilesField, }); return { ...block, props: { ...block.props, - url: `${process.env.SERVER_URL}/files/${signedPath}`, + url, }, }; }); 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 167cdef805..e6cad17142 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 @@ -1,22 +1,16 @@ -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; -import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { type FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util'; -import { type FileService } from 'src/engine/core-modules/file/services/file.service'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; export class WorkspaceMemberQueryResultGetterHandler implements QueryResultGetterHandlerInterface { - constructor( - private readonly fileService: FileService, - private readonly featureFlagService: FeatureFlagService, - private readonly fileUrlService: FileUrlService, - ) {} + constructor(private readonly fileUrlService: FileUrlService) {} async handle( workspaceMember: WorkspaceMemberWorkspaceEntity, @@ -26,41 +20,24 @@ export class WorkspaceMemberQueryResultGetterHandler return workspaceMember; } - if ( - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - workspaceId, - ) - ) { - const fileId = extractFileIdFromUrl( - workspaceMember.avatarUrl, - FileFolder.CorePicture, - ); + const fileId = extractFileIdFromUrl( + workspaceMember.avatarUrl, + FileFolder.CorePicture, + ); - if (!isDefined(fileId)) { - return workspaceMember; - } - - const signedUrl = this.fileUrlService.signFileByIdUrl({ - fileId, - workspaceId, - fileFolder: FileFolder.CorePicture, - }); - - return { - ...workspaceMember, - avatarUrl: signedUrl, - }; + if (!isDefined(fileId)) { + return workspaceMember; } - const signedPath = this.fileService.signFileUrl({ - url: workspaceMember.avatarUrl, + const signedUrl = this.fileUrlService.signFileByIdUrl({ + fileId, workspaceId, + fileFolder: FileFolder.CorePicture, }); return { ...workspaceMember, - avatarUrl: signedPath, + avatarUrl: signedUrl, }; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 1d124947a3..5247ee8de1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -4,9 +4,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; -import { ApplicationRegistrationModule } from 'src/engine/core-modules/application-registration/application-registration.module'; import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity'; import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service'; +import { ApplicationRegistrationModule } from 'src/engine/core-modules/application-registration/application-registration.module'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; @@ -39,7 +39,6 @@ import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module'; import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; @@ -78,7 +77,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; @Module({ imports: [ JwtModule, - FileUploadModule, DataSourceModule, WorkspaceDomainsModule, TokenModule, diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/__tests__/file-storage.service.spec.ts b/packages/twenty-server/src/engine/core-modules/file-storage/__tests__/file-storage.service.spec.ts index eea4a45917..1e23516ae7 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/__tests__/file-storage.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/__tests__/file-storage.service.spec.ts @@ -1,8 +1,6 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Readable } from 'stream'; - import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; @@ -74,81 +72,6 @@ describe('FileStorageService', () => { mockFileStorageDriverFactory.getCurrentDriver.mockReturnValue(mockDriver); }); - describe('writeFileLegacy', () => { - it('should delegate to the current driver', async () => { - const writeParams = { - file: Buffer.from('test content'), - name: 'test.txt', - folder: 'documents', - mimeType: 'text/plain', - }; - - mockDriver.writeFile.mockResolvedValue(undefined); - - await service.writeFileLegacy(writeParams); - - expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled(); - expect(mockDriver.writeFile).toHaveBeenCalledWith({ - filePath: 'documents/test.txt', - sourceFile: writeParams.file, - mimeType: 'text/plain', - }); - }); - - it('should handle write errors', async () => { - const writeParams = { - file: 'test content', - name: 'test.txt', - folder: 'documents', - mimeType: 'text/plain', - }; - - const error = new Error('Write failed'); - - mockDriver.writeFile.mockRejectedValue(error); - - await expect(service.writeFileLegacy(writeParams)).rejects.toThrow( - 'Write failed', - ); - expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled(); - }); - }); - - describe('readFileLegacy', () => { - it('should delegate to the current driver', async () => { - const readParams = { - filePath: 'documents/test.txt', - }; - - const mockStream = new Readable(); - - mockDriver.readFile.mockResolvedValue(mockStream); - - const result = await service.readFileLegacy(readParams); - - expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled(); - expect(mockDriver.readFile).toHaveBeenCalledWith({ - filePath: 'documents/test.txt', - }); - expect(result).toBe(mockStream); - }); - - it('should handle read errors', async () => { - const readParams = { - filePath: 'documents/test.txt', - }; - - const error = new Error('Read failed'); - - mockDriver.readFile.mockRejectedValue(error); - - await expect(service.readFileLegacy(readParams)).rejects.toThrow( - 'Read failed', - ); - expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled(); - }); - }); - describe('deleteLegacy', () => { it('should delegate to the current driver with filename', async () => { const deleteParams = { diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts index 4ac30014ff..1ab6159994 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { mkdir, readdir, readFile, stat } from 'fs/promises'; import { basename, dirname, join } from 'path'; import { type Readable } from 'stream'; -import { isObject } from '@sniptt/guards'; -import { FileFolder, Sources } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { Like, Repository, type QueryRunner } from 'typeorm'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; @@ -45,23 +43,6 @@ export class FileStorageService { ).replace(/\/+/g, '/'); } - writeFileLegacy(params: { - file: string | Buffer | Uint8Array; - name: string; - folder: string; - mimeType: string | undefined; - }): Promise { - const { file, name, folder, mimeType } = params; - - const driver = this.fileStorageDriverFactory.getCurrentDriver(); - - return driver.writeFile({ - filePath: `${folder}/${name}`, - sourceFile: file, - mimeType, - }); - } - async writeFile({ sourceFile, mimeType, @@ -133,12 +114,6 @@ export class FileStorageService { }); } - readFileLegacy(params: { filePath: string }): Promise { - const driver = this.fileStorageDriverFactory.getCurrentDriver(); - - return driver.readFile(params); - } - readFile(params: ResourceIdentifier): Promise { const driver = this.fileStorageDriverFactory.getCurrentDriver(); @@ -147,56 +122,6 @@ export class FileStorageService { return driver.readFile({ filePath: onStoragePath }); } - async writeFolderLegacy(sources: Sources, folderPath: string): Promise { - for (const key of Object.keys(sources)) { - if (isObject(sources[key])) { - await this.writeFolderLegacy(sources[key], join(folderPath, key)); - continue; - } - await this.writeFileLegacy({ - file: sources[key], - name: key, - folder: folderPath, - mimeType: undefined, - }); - } - } - - async readFolderLegacy( - folderPath: string, - localTempPath?: string, - ): Promise { - const driver = this.fileStorageDriverFactory.getCurrentDriver(); - const tempDir = localTempPath || `/tmp/twenty-read-folder-${Date.now()}`; - - await mkdir(tempDir, { recursive: true }); - - await driver.downloadFolder({ - onStoragePath: folderPath, - localPath: tempDir, - }); - - return this.readLocalFolderToSources(tempDir); - } - - private async readLocalFolderToSources(localPath: string): Promise { - const sources: Sources = {}; - const entries = await readdir(localPath); - - for (const entry of entries) { - const entryPath = join(localPath, entry); - const stats = await stat(entryPath); - - if (stats.isFile()) { - sources[entry] = await readFile(entryPath, 'utf8'); - } else { - sources[entry] = await this.readLocalFolderToSources(entryPath); - } - } - - return sources; - } - downloadFile( params: ResourceIdentifier & { localPath: string }, ): Promise { diff --git a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.spec.ts index f0772dd05d..4562d7c264 100644 --- a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.spec.ts @@ -15,7 +15,6 @@ import { FileExceptionCode, } from 'src/engine/core-modules/file/file.exception'; import { FileApiExceptionFilter } from 'src/engine/core-modules/file/filters/file-api-exception.filter'; -import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard'; import { FileByIdGuard } from 'src/engine/core-modules/file/guards/file-by-id.guard'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; @@ -36,7 +35,6 @@ const createMockStream = (): Readable => { describe('FileController', () => { let controller: FileController; let fileService: FileService; - const mock_FilePathGuard: CanActivate = { canActivate: jest.fn(() => true) }; const mock_FileByIdGuard: CanActivate = { canActivate: jest.fn(() => true) }; const mock_PublicEndpointGuard: CanActivate = { canActivate: jest.fn(() => true), @@ -52,15 +50,12 @@ describe('FileController', () => { { provide: FileService, useValue: { - getFileStream: jest.fn(), getFileStreamById: jest.fn(), getFileStreamByPath: jest.fn(), }, }, ], }) - .overrideGuard(FilePathGuard) - .useValue(mock_FilePathGuard) .overrideGuard(FileByIdGuard) .useValue(mock_FileByIdGuard) .overrideGuard(PublicEndpointGuard) @@ -79,50 +74,6 @@ describe('FileController', () => { expect(controller).toBeDefined(); }); - describe('getFile', () => { - it('should extract folder, token and filename from 3-segment path', async () => { - const mockStream = createMockStream(); - - jest.spyOn(fileService, 'getFileStream').mockResolvedValue(mockStream); - - const mockRequest = { - path: '/files/attachment/test-token/test-file.csv', - workspaceId: 'workspace-id', - } as any; - - const mockResponse = {} as any; - - await controller.getFile(mockResponse, mockRequest); - - expect(fileService.getFileStream).toHaveBeenCalledWith( - 'attachment', - 'test-file.csv', - 'workspace-id', - ); - }); - - it('should extract folder with size, token and filename from 4-segment path', async () => { - const mockStream = createMockStream(); - - jest.spyOn(fileService, 'getFileStream').mockResolvedValue(mockStream); - - const mockRequest = { - path: '/files/profile-picture/original/test-token/avatar.jpg', - workspaceId: 'workspace-id', - } as any; - - const mockResponse = {} as any; - - await controller.getFile(mockResponse, mockRequest); - - expect(fileService.getFileStream).toHaveBeenCalledWith( - 'profile-picture/original', - 'avatar.jpg', - 'workspace-id', - ); - }); - }); - describe('getFileById', () => { it('should call fileService.getFileStreamById and pipe the result', async () => { const mockStream = createMockStream(); diff --git a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts index 65f7439168..7999617c6d 100644 --- a/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts @@ -23,15 +23,13 @@ import { FileExceptionCode, } from 'src/engine/core-modules/file/file.exception'; import { FileApiExceptionFilter } from 'src/engine/core-modules/file/filters/file-api-exception.filter'; -import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { extractFileInfoFromRequest } from 'src/engine/core-modules/file/utils/extract-file-info-from-request.utils'; -import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; -import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; import { FileByIdGuard, SupportedFileFolder, } from 'src/engine/core-modules/file/guards/file-by-id.guard'; +import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; @Controller() @UseFilters(FileApiExceptionFilter) @@ -83,47 +81,6 @@ export class FileController { } } - @Get('files/*path') - @UseGuards(FilePathGuard, NoPermissionGuard) - async getFile(@Res() res: Response, @Req() req: Request) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const workspaceId = (req as any)?.workspaceId; - - const { rawFolder, filename } = extractFileInfoFromRequest(req); - - try { - const fileStream = await this.fileService.getFileStream( - rawFolder, - filename, - workspaceId, - ); - - fileStream.on('error', () => { - throw new FileException( - 'Error streaming file from storage', - FileExceptionCode.INTERNAL_SERVER_ERROR, - ); - }); - - fileStream.pipe(res); - } catch (error) { - if ( - error instanceof FileStorageException && - error.code === FileStorageExceptionCode.FILE_NOT_FOUND - ) { - throw new FileException( - 'File not found', - FileExceptionCode.FILE_NOT_FOUND, - ); - } - - throw new FileException( - `Error retrieving file: ${error.message}`, - FileExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - } - @Get('file/:fileFolder/:id') @UseGuards(FileByIdGuard, NoPermissionGuard) async getFileById( diff --git a/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/file-ai-chat.module.ts b/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/file-ai-chat.module.ts new file mode 100644 index 0000000000..f8f8a4f9ad --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/file-ai-chat.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { FileAIChatResolver } from 'src/engine/core-modules/file/file-ai-chat/resolvers/file-ai-chat.resolver'; +import { FileAIChatService } from 'src/engine/core-modules/file/file-ai-chat/services/file-ai-chat.service'; +import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; + +@Module({ + imports: [FileUrlModule, ApplicationModule, PermissionsModule], + providers: [FileAIChatService, FileAIChatResolver], + exports: [FileAIChatService], +}) +export class FileAIChatModule {} diff --git a/packages/twenty-server/src/engine/core-modules/file/resolvers/file.resolver.ts b/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/resolvers/file-ai-chat.resolver.ts similarity index 54% rename from packages/twenty-server/src/engine/core-modules/file/resolvers/file.resolver.ts rename to packages/twenty-server/src/engine/core-modules/file/file-ai-chat/resolvers/file-ai-chat.resolver.ts index ed5d325ec1..8b41c632d2 100644 --- a/packages/twenty-server/src/engine/core-modules/file/resolvers/file.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/resolvers/file-ai-chat.resolver.ts @@ -7,9 +7,8 @@ import { PermissionFlagType } from 'twenty-shared/constants'; import type { FileUpload } from 'graphql-upload/processRequest.mjs'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; -import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; -import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto'; -import { FileMetadataService } from 'src/engine/core-modules/file/services/file-metadata.service'; +import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto'; +import { FileAIChatService } from 'src/engine/core-modules/file/file-ai-chat/services/file-ai-chat.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -22,46 +21,24 @@ import { streamToBuffer } from 'src/utils/stream-to-buffer'; @UsePipes(ResolverValidationPipe) @UseFilters(PreventNestToAutoLogGraphqlErrorsFilter) @MetadataResolver() -export class FileResolver { - constructor(private readonly fileMetadataService: FileMetadataService) {} +export class FileAIChatResolver { + constructor(private readonly fileAIChatService: FileAIChatService) {} - @Mutation(() => FileDTO, { - deprecationReason: 'Use specific file service instead', - }) + @Mutation(() => FileWithSignedUrlDTO) @UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE)) - async createFile( - @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + async uploadAIChatFile( + @AuthWorkspace() + { id: workspaceId }: WorkspaceEntity, @Args({ name: 'file', type: () => GraphQLUpload }) - { createReadStream, filename, mimetype }: FileUpload, - ): Promise { + { createReadStream, filename }: FileUpload, + ): Promise { const stream = createReadStream(); const buffer = await streamToBuffer(stream); - return this.fileMetadataService.createFile({ + return await this.fileAIChatService.uploadFile({ file: buffer, filename, - mimeType: mimetype, workspaceId, }); } - - @Mutation(() => FileDTO, { - deprecationReason: '', - }) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE)) - async deleteFile( - @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - @Args('fileId', { type: () => UUIDScalarType }) fileId: string, - ): Promise { - const deletedFile = await this.fileMetadataService.deleteFileById( - fileId, - workspaceId, - ); - - if (!deletedFile) { - throw new Error(`File with id ${fileId} not found`); - } - - return deletedFile; - } } diff --git a/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/services/file-ai-chat.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/services/file-ai-chat.service.ts new file mode 100644 index 0000000000..44f92e05ff --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/file/file-ai-chat/services/file-ai-chat.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; + +import { isNonEmptyString } from '@sniptt/guards'; +import { FileFolder } from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; +import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { FileWithSignedUrlDTO } from 'src/engine/core-modules/file/dtos/file-with-sign-url.dto'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; +import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils'; +import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils'; +@Injectable() +export class FileAIChatService { + constructor( + private readonly fileStorageService: FileStorageService, + private readonly applicationService: ApplicationService, + private readonly fileUrlService: FileUrlService, + ) {} + + async uploadFile({ + file, + filename, + workspaceId, + }: { + file: Buffer; + filename: string; + workspaceId: string; + }): Promise { + const { mimeType, ext } = await extractFileInfo({ + file, + filename, + }); + + const sanitizedFile = sanitizeFile({ file, ext, mimeType }); + + const fileId = v4(); + const name = `${fileId}${isNonEmptyString(ext) ? `.${ext}` : ''}`; + + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); + + const savedFile = await this.fileStorageService.writeFile({ + sourceFile: sanitizedFile, + resourcePath: name, + mimeType, + fileFolder: FileFolder.AgentChat, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, + workspaceId, + fileId, + settings: { + isTemporaryFile: false, + toDelete: false, + }, + }); + + return { + ...savedFile, + url: this.fileUrlService.signFileByIdUrl({ + fileId, + workspaceId, + fileFolder: FileFolder.AgentChat, + }), + }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/dtos/signed-file.dto.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/dtos/signed-file.dto.ts deleted file mode 100644 index 8e9a4a617a..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/dtos/signed-file.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -@ObjectType('SignedFile') -export class SignedFileDTO { - @Field(() => String) - path: string; - - @Field(() => String) - token: string; -} diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/file-upload.module.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/file-upload.module.ts deleted file mode 100644 index b0ccf20bba..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/file-upload.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver'; -import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { FileModule } from 'src/engine/core-modules/file/file.module'; -import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module'; -import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; - -@Module({ - imports: [FileModule, PermissionsModule, SecureHttpClientModule], - providers: [FileUploadService, FileUploadResolver], - exports: [FileUploadService, FileUploadResolver], -}) -export class FileUploadModule {} diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver.spec.ts deleted file mode 100644 index 2405483079..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -jest.mock('graphql-upload/GraphQLUpload.mjs', () => ({ - __esModule: true, - default: {}, -})); - -jest.mock('graphql-upload/processRequest.mjs', () => ({ - __esModule: true, - FileUpload: {}, -})); - -import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; - -import { FileUploadResolver } from './file-upload.resolver'; - -describe('FileUploadResolver', () => { - let resolver: FileUploadResolver; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - FileUploadResolver, - { - provide: FileUploadService, - useValue: {}, - }, - { - provide: TwentyConfigService, - useValue: {}, - }, - { - provide: PermissionsService, - useValue: {}, - }, - ], - }).compile(); - - resolver = module.get(FileUploadResolver); - }); - - it('should be defined', () => { - expect(resolver).toBeDefined(); - }); -}); diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver.ts deleted file mode 100644 index 94346c1cdd..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; -import { Args, Mutation } from '@nestjs/graphql'; - -import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; -import { PermissionFlagType } from 'twenty-shared/constants'; -import { FileFolder } from 'twenty-shared/types'; - -import type { FileUpload } from 'graphql-upload/processRequest.mjs'; - -import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto'; -import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; -import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; -import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; -import { streamToBuffer } from 'src/utils/stream-to-buffer'; - -@UseGuards(WorkspaceAuthGuard) -@UsePipes(ResolverValidationPipe) -@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter) -@MetadataResolver() -export class FileUploadResolver { - constructor(private readonly fileUploadService: FileUploadService) {} - - @Mutation(() => SignedFileDTO, { - deprecationReason: 'Use uploadFilesFieldFile instead', - }) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE)) - async uploadFile( - @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - @Args({ name: 'file', type: () => GraphQLUpload }) - { createReadStream, filename, mimetype }: FileUpload, - @Args('fileFolder', { type: () => FileFolder, nullable: true }) - fileFolder: FileFolder, - ): Promise { - const stream = createReadStream(); - const buffer = await streamToBuffer(stream); - - const { files } = await this.fileUploadService.uploadFile({ - file: buffer, - filename, - mimeType: mimetype, - fileFolder, - workspaceId, - }); - - if (!files.length) { - throw new Error('Failed to upload file'); - } - - return files[0]; - } - - @Mutation(() => SignedFileDTO) - @UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE)) - async uploadImage( - @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - @Args({ name: 'file', type: () => GraphQLUpload }) - { createReadStream, filename, mimetype }: FileUpload, - @Args('fileFolder', { type: () => FileFolder, nullable: true }) - fileFolder: FileFolder, - ): Promise { - const stream = createReadStream(); - const buffer = await streamToBuffer(stream); - - const { files } = await this.fileUploadService.uploadImage({ - file: buffer, - filename, - mimeType: mimetype, - fileFolder, - workspaceId, - }); - - if (!files.length) { - throw new Error('Failed to upload image'); - } - - return files[0]; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts deleted file mode 100644 index 5ac85c315a..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/file-upload/services/file-upload.service.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import FileType from 'file-type'; -import sharp from 'sharp'; -import { FileFolder } from 'twenty-shared/types'; -import { v4 } from 'uuid'; - -import { settings } from 'src/engine/constants/settings'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils'; -import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils'; -import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; -import { getCropSize, getImageBufferFromUrl } from 'src/utils/image'; - -export type SignedFile = { path: string; token: string }; - -export type SignedFilesResult = { - name: string; - mimeType: string | undefined; - files: SignedFile[]; -}; - -@Injectable() -export class FileUploadService { - private readonly logger = new Logger(FileUploadService.name); - - constructor( - private readonly fileStorage: FileStorageService, - private readonly fileService: FileService, - private readonly secureHttpClientService: SecureHttpClientService, - ) {} - - private async _uploadFile({ - file, - filename, - mimeType, - folder, - }: { - file: Buffer | Uint8Array | string; - filename: string; - mimeType: string | undefined; - folder: string; - }) { - await this.fileStorage.writeFileLegacy({ - file, - name: filename, - mimeType, - folder, - }); - } - - /** - * @deprecated Use uploadWorkspaceRecordFile if uploading workspace records-scoped files. Or create your dedicated upload file service. - */ - async uploadFile({ - file, - filename, - mimeType, - fileFolder, - workspaceId, - }: { - file: Buffer | Uint8Array | string; - filename: string; - mimeType: string | undefined; - fileFolder: FileFolder; - workspaceId: string; - }): Promise { - const { ext, name } = buildFileInfo(filename); - const folder = this.getWorkspaceFolderName(workspaceId, fileFolder); - - await this._uploadFile({ - file: sanitizeFile({ file, ext, mimeType }), - filename: name, - mimeType, - folder, - }); - - const signedPayload = this.fileService.encodeFileToken({ - filename: name, - workspaceId: workspaceId, - }); - - return { - name, - mimeType, - files: [{ path: `${fileFolder}/${name}`, token: signedPayload }], - }; - } - - async uploadImageFromUrl({ - imageUrl, - fileFolder, - workspaceId, - }: { - imageUrl: string; - fileFolder: FileFolder; - workspaceId: string; - }) { - const imageData = await this.fetchImageBufferFromUrl(imageUrl).catch( - (error) => { - this.logger.warn( - `Failed to fetch image from URL: ${imageUrl} — ${error instanceof Error ? error.message : String(error)}`, - ); - - return null; - }, - ); - - if (!imageData) { - return { name: '', mimeType: undefined, files: [] }; - } - - return await this.uploadImage({ - file: imageData.buffer, - filename: `${v4()}.${imageData.extension}`, - mimeType: imageData.mimeType, - fileFolder, - workspaceId, - }); - } - - private async fetchImageBufferFromUrl(imageUrl: string): Promise<{ - buffer: Buffer; - extension: string; - mimeType: string; - } | null> { - const httpClient = this.secureHttpClientService.getHttpClient({ - retries: 2, - shouldResetTimeout: true, - }); - - const buffer = await getImageBufferFromUrl(imageUrl, httpClient); - - if (!buffer || buffer.length === 0) { - return null; - } - - const type = await FileType.fromBuffer(buffer); - - if (!type || !type.ext || !type.mime || !type.mime.startsWith('image/')) { - throw new Error(`Invalid image type for URL: ${imageUrl}`); - } - - return { buffer, extension: type.ext, mimeType: type.mime }; - } - - async uploadImage({ - file, - filename, - mimeType, - fileFolder, - workspaceId, - }: { - file: Buffer | Uint8Array | string; - filename: string; - mimeType: string | undefined; - fileFolder: FileFolder; - workspaceId: string; - }): Promise { - const { name } = buildFileInfo(filename); - - const cropSizes = settings.storage.imageCropSizes[fileFolder]; - - if (!cropSizes) { - throw new Error(`No crop sizes found for ${fileFolder}`); - } - - const sizes = cropSizes.map((shortSize) => getCropSize(shortSize)); - const images = await Promise.all( - sizes.map((size) => - sharp(file).resize({ - [size?.type || 'width']: size?.value ?? undefined, - }), - ), - ); - - const files: Array = []; - - await Promise.all( - images.map(async (image, index) => { - const buffer = await image.toBuffer(); - const folder = this.getWorkspaceFolderName(workspaceId, fileFolder); - - const token = this.fileService.encodeFileToken({ - filename: name, - workspaceId: workspaceId, - }); - - files.push({ - path: `${fileFolder}/${cropSizes[index]}/${name}`, - token, - }); - - return this._uploadFile({ - file: buffer, - filename: `${cropSizes[index]}/${name}`, - mimeType, - folder, - }); - }), - ); - - return { - name, - mimeType, - files, - }; - } - - private getWorkspaceFolderName(workspaceId: string, fileFolder: FileFolder) { - return `workspace-${workspaceId}/${fileFolder}`; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/file/file.module.ts b/packages/twenty-server/src/engine/core-modules/file/file.module.ts index 394f195db5..8469a036c4 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file.module.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; +import { FileAIChatModule } from 'src/engine/core-modules/file/file-ai-chat/file-ai-chat.module'; import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard'; import { FileDeletionJob } from 'src/engine/core-modules/file/jobs/file-deletion.job'; import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/jobs/file-workspace-folder-deletion.job'; @@ -16,13 +17,10 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { FileController } from './controllers/file.controller'; import { FileEntity } from './entities/file.entity'; import { FileCorePictureModule } from './file-core-picture/file-core-picture.module'; -import { FileUploadService } from './file-upload/services/file-upload.service'; import { FileUrlModule } from './file-url/file-url.module'; import { FileWorkflowModule } from './file-workflow/file-workflow.module'; import { FilesFieldModule } from './files-field/files-field.module'; import { FileByIdGuard } from './guards/file-by-id.guard'; -import { FileResolver } from './resolvers/file.resolver'; -import { FileMetadataService } from './services/file-metadata.service'; import { FileService } from './services/file.service'; @Module({ @@ -35,28 +33,25 @@ import { FileService } from './services/file.service'; FilesFieldModule, FileCorePictureModule, FileWorkflowModule, + FileAIChatModule, SecureHttpClientModule, ], providers: [ FileService, - FileMetadataService, - FileResolver, FilePathGuard, FileByIdGuard, FileAttachmentListener, FileWorkspaceMemberListener, FileWorkspaceFolderDeletionJob, FileDeletionJob, - FileUploadService, ], exports: [ FileService, - FileMetadataService, FileUrlModule, FilesFieldModule, FileCorePictureModule, FileWorkflowModule, - FileUploadService, + FileAIChatModule, ], controllers: [FileController], }) diff --git a/packages/twenty-server/src/engine/core-modules/file/guards/file-by-id.guard.ts b/packages/twenty-server/src/engine/core-modules/file/guards/file-by-id.guard.ts index 01885343ff..2e41991f51 100644 --- a/packages/twenty-server/src/engine/core-modules/file/guards/file-by-id.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/file/guards/file-by-id.guard.ts @@ -11,6 +11,7 @@ export const SUPPORTED_FILE_FOLDERS = [ FileFolder.CorePicture, FileFolder.FilesField, FileFolder.Workflow, + FileFolder.AgentChat, ] as const; export type SupportedFileFolder = (typeof SUPPORTED_FILE_FOLDERS)[number]; diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts deleted file mode 100644 index a428de4d3a..0000000000 --- a/packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { FileFolder } from 'twenty-shared/types'; -import { extractFolderPathFilenameAndTypeOrThrow } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type FileDTO } from 'src/engine/core-modules/file/dtos/file.dto'; -import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; - -import { FileService } from './file.service'; - -@Injectable() -export class FileMetadataService { - constructor( - @InjectRepository(FileEntity) - private readonly fileRepository: Repository, - private readonly fileService: FileService, - private readonly fileUploadService: FileUploadService, - ) {} - - /** - * @deprecated - */ - async createFile({ - file, - filename, - mimeType, - workspaceId, - }: { - file: Buffer; - filename: string; - mimeType: string; - workspaceId: string; - }): Promise { - const { files } = await this.fileUploadService.uploadFile({ - file, - filename, - mimeType, - fileFolder: FileFolder.File, - workspaceId, - }); - - if (!files.length) { - throw new Error('Failed to upload file'); - } - - const createdFile = this.fileRepository.create({ - path: files[0].path, - size: file.length, - workspaceId, - }); - - const savedFile = await this.fileRepository.save(createdFile); - - return savedFile; - } - - /** - * @deprecated - */ - async deleteFileById( - id: string, - workspaceId: string, - ): Promise { - const file = await this.fileRepository.findOne({ - where: { id, workspaceId }, - }); - - if (!file) { - return null; - } - - const { folderPath, filename } = extractFolderPathFilenameAndTypeOrThrow( - file.path, - ); - - try { - if (file.path) { - await this.fileService.deleteFile({ - folderPath, - filename, - workspaceId, - }); - } - - await this.fileRepository.delete(file.id); - - return file; - } catch (error) { - throw new Error(`Failed to delete file ${id}: ${error.message}`); - } - } -} diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts index 7a4def59cf..c12d38cb22 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.spec.ts @@ -15,18 +15,11 @@ jest.mock('uuid', () => ({ describe('FileService', () => { let service: FileService; - let fileStorageService: FileStorageService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FileService, - { - provide: FileStorageService, - useValue: { - copyLegacy: jest.fn(), - }, - }, { provide: TwentyConfigService, useValue: {}, @@ -35,6 +28,10 @@ describe('FileService', () => { provide: JwtWrapperService, useValue: {}, }, + { + provide: FileStorageService, + useValue: {}, + }, { provide: getRepositoryToken(FileEntity), useValue: {}, @@ -47,35 +44,9 @@ describe('FileService', () => { }).compile(); service = module.get(FileService); - fileStorageService = module.get(FileStorageService); }); it('should be defined', () => { expect(service).toBeDefined(); }); - - it('copyFileFromWorkspaceToWorkspace - should copy a file to a new workspace', async () => { - const result = await service.copyFileFromWorkspaceToWorkspace( - 'workspaceId', - 'path/to/file', - 'newWorkspaceId', - ); - - expect(fileStorageService.copyLegacy).toHaveBeenCalledWith({ - from: { - folderPath: 'workspace-workspaceId/path/to', - filename: 'file', - }, - to: { - folderPath: 'workspace-newWorkspaceId/path/to', - filename: 'mocked-uuid', - }, - }); - - expect(result).toEqual([ - 'workspace-newWorkspaceId', - 'path/to', - 'mocked-uuid', - ]); - }); }); diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts index 3f9e93e73e..9bfc73ace8 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { basename, dirname, extname } from 'path'; import { type Readable } from 'stream'; import { isNonEmptyString } from '@sniptt/guards'; @@ -11,7 +10,6 @@ import { extractFolderPathFilenameAndTypeOrThrow, } from 'twenty-shared/utils'; import { Like, Repository } from 'typeorm'; -import { v4 as uuidV4 } from 'uuid'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { @@ -23,6 +21,7 @@ import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { streamToBuffer } from 'src/utils/stream-to-buffer'; @Injectable() export class FileService { @@ -36,18 +35,6 @@ export class FileService { private readonly applicationRepository: Repository, ) {} - async getFileStream( - folderPath: string, - filename: string, - workspaceId: string, - ): Promise { - const workspaceFolderPath = `workspace-${workspaceId}/${folderPath}`; - - return await this.fileStorageService.readFileLegacy({ - filePath: `${workspaceFolderPath}/${filename}`, - }); - } - async getFileStreamByPath({ workspaceId, applicationId, @@ -106,6 +93,45 @@ export class FileService { }); } + async getFileContentById({ + fileId, + workspaceId, + fileFolder, + }: { + fileId: string; + workspaceId: string; + fileFolder: FileFolder; + }): Promise<{ buffer: Buffer; mimeType: string }> { + const file = await this.fileRepository.findOneOrFail({ + where: { + id: fileId, + workspaceId, + path: Like(`${fileFolder}/%`), + }, + }); + + const application = await this.applicationRepository.findOneOrFail({ + where: { + id: file.applicationId, + workspaceId, + }, + }); + + const stream = await this.fileStorageService.readFile({ + resourcePath: removeFileFolderFromFileEntityPath(file.path), + fileFolder, + applicationUniversalIdentifier: application.universalIdentifier, + workspaceId, + }); + + const buffer = await streamToBuffer(stream); + + return { + buffer, + mimeType: file.mimeType ?? 'application/octet-stream', + }; + } + signFileUrl({ url, workspaceId }: { url: string; workspaceId: string }) { if (!isNonEmptyString(url)) { return url; @@ -177,30 +203,4 @@ export class FileService { folderPath: workspaceFolderPath, }); } - - async copyFileFromWorkspaceToWorkspace( - fromWorkspaceId: string, - fromPath: string, - toWorkspaceId: string, - ) { - const subFolder = dirname(fromPath); - const fromWorkspaceFolderPath = `workspace-${fromWorkspaceId}`; - const toWorkspaceFolderPath = `workspace-${toWorkspaceId}`; - const fromFilename = basename(fromPath); - - const toFilename = uuidV4() + extname(fromFilename); - - await this.fileStorageService.copyLegacy({ - from: { - folderPath: `${fromWorkspaceFolderPath}/${subFolder}`, - filename: fromFilename, - }, - to: { - folderPath: `${toWorkspaceFolderPath}/${subFolder}`, - filename: toFilename, - }, - }); - - return [toWorkspaceFolderPath, subFolder, toFilename]; - } } diff --git a/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts b/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts index a03175740b..886a4f72ec 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tool.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -20,6 +21,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou MessagingImportManagerModule, MessagingSendManagerModule, TypeOrmModule.forFeature([FileEntity]), + ApplicationModule, FeatureFlagModule, FileModule, JwtModule, diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts index b0b3a75759..775b3d7eab 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema.ts @@ -6,9 +6,9 @@ export const CodeInterpreterInputZodSchema = z.object({ .array( z.object({ filename: z.string().describe('Name of the file'), - url: z + fileId: z .string() - .describe('URL of the file to include (from user attachments)'), + .describe('ID of the uploaded file (from user attachments)'), }), ) .optional() diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts index 874b1a3e62..e1be014913 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts @@ -15,18 +15,23 @@ import { type OutputFile, } from 'src/engine/core-modules/code-interpreter/drivers/interfaces/code-interpreter-driver.interface'; +import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; import { type AccessTokenJwtPayload, JwtTokenTypeEnum, } from 'src/engine/core-modules/auth/types/auth-context.type'; import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; import { CodeInterpreterInputZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema'; import { TWENTY_MCP_HELPER } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const'; -import { type CodeInterpreterInput } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type'; +import { + type CodeInterpreterFileInput, + type CodeInterpreterInput, +} from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type'; import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { @@ -49,6 +54,8 @@ export class CodeInterpreterTool implements Tool { private readonly codeInterpreterService: CodeInterpreterService, private readonly fileStorageService: FileStorageService, private readonly fileService: FileService, + private readonly fileUrlService: FileUrlService, + private readonly applicationService: ApplicationService, private readonly secureHttpClientService: SecureHttpClientService, private readonly twentyConfigService: TwentyConfigService, private readonly jwtWrapperService: JwtWrapperService, @@ -94,7 +101,7 @@ export class CodeInterpreterTool implements Tool { ); try { - const inputFiles = await this.downloadInputFiles(files); + const inputFiles = await this.downloadInputFiles(files, workspaceId); this.logger.log( `Executing code interpreter with ${inputFiles.length} input files`, @@ -251,74 +258,43 @@ export class CodeInterpreterTool implements Tool { } private async downloadInputFiles( - files?: { filename: string; url: string }[], + files?: CodeInterpreterFileInput[], + workspaceId?: string, ): Promise { if (!files || files.length === 0) { return []; } const inputFiles: InputFile[] = []; - const serverUrl = this.twentyConfigService.get('SERVER_URL'); for (const file of files) { try { - if (file.url.startsWith('data:')) { - const parsed = this.parseDataUrl(file.url); - - if (parsed) { - inputFiles.push({ - filename: file.filename, - content: parsed.content, - mimeType: parsed.mimeType, - }); - } + if (!workspaceId) { + this.logger.warn( + `Cannot resolve file ${file.filename}: workspaceId is required`, + ); continue; } - // Internal file downloads (from the server itself) use a plain client; - // external URLs go through the SSRF-protected client - const isInternalFileUrl = file.url.startsWith(serverUrl); - const httpClient = isInternalFileUrl - ? this.secureHttpClientService.getInternalHttpClient() - : this.secureHttpClientService.getHttpClient(); - - const response = await httpClient.get(file.url, { - responseType: 'arraybuffer', - timeout: 30_000, + const { buffer, mimeType } = await this.fileService.getFileContentById({ + fileId: file.fileId, + workspaceId, + fileFolder: FileFolder.AgentChat, }); inputFiles.push({ filename: file.filename, - content: Buffer.from(response.data), - mimeType: - response.headers['content-type'] ?? 'application/octet-stream', + content: buffer, + mimeType, }); } catch (error) { - this.logger.warn(`Failed to download file ${file.filename}`, error); + this.logger.warn(`Failed to resolve file ${file.filename}`, error); } } return inputFiles; } - private parseDataUrl( - dataUrl: string, - ): { content: Buffer; mimeType: string } | null { - // Format: data:{mimeType};base64,{base64data} - const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/); - - if (!match) { - return null; - } - - const [, mimeType, base64Data] = match; - - return { - content: Buffer.from(base64Data, 'base64'), - mimeType, - }; - } - private generateSessionToken( workspaceId: string, userId?: string, @@ -349,30 +325,42 @@ export class CodeInterpreterTool implements Tool { workspaceId: string, executionId: string, ): Promise { - const subFolder = `${FileFolder.AgentChat}/code-interpreter/${executionId}`; - const folder = `workspace-${workspaceId}/${subFolder}`; - const sanitizedFilename = path.basename(file.filename); try { - await this.fileStorageService.writeFileLegacy({ - file: file.content, - name: sanitizedFilename, + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const fileId = v4(); + const resourcePath = `code-interpreter/${executionId}/${fileId}-${sanitizedFilename}`; + + const savedFile = await this.fileStorageService.writeFile({ + sourceFile: file.content, mimeType: file.mimeType, - folder, - }); - - const filePath = `${subFolder}/${sanitizedFilename}`; - const signedPath = this.fileService.signFileUrl({ - url: filePath, + fileFolder: FileFolder.AgentChat, + applicationUniversalIdentifier: + workspaceCustomFlatApplication.universalIdentifier, workspaceId, + resourcePath, + fileId, + settings: { + isTemporaryFile: false, + toDelete: false, + }, }); - const serverUrl = this.twentyConfigService.get('SERVER_URL'); + const signedUrl = this.fileUrlService.signFileByIdUrl({ + fileId: savedFile.id, + workspaceId, + fileFolder: FileFolder.AgentChat, + }); return { + fileId: savedFile.id, filename: sanitizedFilename, - url: `${serverUrl}/files/${signedPath}`, + url: signedUrl, mimeType: file.mimeType, }; } catch (error) { @@ -388,12 +376,9 @@ export class CodeInterpreterTool implements Tool { executionId: string, alreadyUploadedFiles: CodeExecutionFile[], ): Promise { - const subFolder = `${FileFolder.AgentChat}/code-interpreter/${executionId}`; - const folder = `workspace-${workspaceId}/${subFolder}`; - const outputFileUrls: CodeExecutionFile[] = [...alreadyUploadedFiles]; const uploadedFilenames = new Set( - alreadyUploadedFiles.map((f) => f.filename), + alreadyUploadedFiles.map((uploadedFile) => uploadedFile.filename), ); for (const file of files) { @@ -403,32 +388,14 @@ export class CodeInterpreterTool implements Tool { continue; } - try { - await this.fileStorageService.writeFileLegacy({ - file: file.content, - name: sanitizedFilename, - mimeType: file.mimeType, - folder, - }); + const uploadedFile = await this.uploadSingleFile( + file, + workspaceId, + executionId, + ); - const filePath = `${subFolder}/${sanitizedFilename}`; - const signedPath = this.fileService.signFileUrl({ - url: filePath, - workspaceId, - }); - - const serverUrl = this.twentyConfigService.get('SERVER_URL'); - - outputFileUrls.push({ - filename: sanitizedFilename, - url: `${serverUrl}/files/${signedPath}`, - mimeType: file.mimeType, - }); - } catch (error) { - this.logger.warn( - `Failed to upload output file ${file.filename}`, - error, - ); + if (uploadedFile) { + outputFileUrls.push(uploadedFile); } } diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type.ts index 31341c7374..1d3fa26469 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type.ts @@ -1,6 +1,6 @@ export type CodeInterpreterFileInput = { filename: string; - url: string; + fileId: string; }; export type CodeInterpreterInput = { diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts index 2ec99e4a2a..1f93b0a5ea 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/email-tool/email-composer.service.ts @@ -1,32 +1,25 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Readable } from 'stream'; - import { render, toPlainText } from '@react-email/render'; import DOMPurify from 'dompurify'; import { reactMarkupFromJSON } from 'twenty-emails'; -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; -import { - extractFolderPathFilenameAndTypeOrThrow, - isDefined, - isValidUuid, -} from 'twenty-shared/utils'; +import { FileFolder } from 'twenty-shared/types'; +import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { WorkflowAttachment } from 'twenty-shared/workflow'; import { In, type Repository } from 'typeorm'; import { z } from 'zod'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { EmailToolException, EmailToolExceptionCode, } from 'src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception'; -import { type EmailComposerResult } from 'src/engine/core-modules/tool/tools/email-tool/types/email-composer-result.type'; -import { type EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type'; +import { EmailComposerResult } from 'src/engine/core-modules/tool/tools/email-tool/types/email-composer-result.type'; +import { EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type'; import { parseCommaSeparatedEmails } from 'src/engine/core-modules/tool/tools/email-tool/utils/parse-comma-separated-emails.util'; -import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool.type'; +import { ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool.type'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -34,7 +27,6 @@ import { MessagingAccountAuthenticationService } from 'src/modules/messaging/mes import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message'; import { parseEmailBody } from 'src/utils/parse-email-body'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; - @Injectable() export class EmailComposerService { private readonly logger = new Logger(EmailComposerService.name); @@ -45,16 +37,8 @@ export class EmailComposerService { @InjectRepository(FileEntity) private readonly fileRepository: Repository, private readonly fileService: FileService, - private readonly featureFlagService: FeatureFlagService, ) {} - private async isOtherFileMigrated(workspaceId: string): Promise { - return this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_OTHER_FILE_MIGRATED, - workspaceId, - ); - } - private async getConnectedAccount( connectedAccountId: string, workspaceId: string, @@ -214,29 +198,11 @@ export class EmailComposerService { const attachments: MessageAttachment[] = []; for (const fileMetadata of files) { - const fileEntity = fileEntityMap.get(fileMetadata.id)!; - - const { folderPath, filename } = extractFolderPathFilenameAndTypeOrThrow( - fileEntity.path, - ); - - const isOtherFileMigrated = await this.isOtherFileMigrated(workspaceId); - - let stream: Readable; - - if (isOtherFileMigrated) { - stream = await this.fileService.getFileStreamById({ - fileId: fileMetadata.id, - workspaceId, - fileFolder: FileFolder.Workflow, - }); - } else { - stream = await this.fileService.getFileStream( - folderPath, - filename, - workspaceId, - ); - } + const stream = await this.fileService.getFileStreamById({ + fileId: fileMetadata.id, + workspaceId, + fileFolder: FileFolder.Workflow, + }); const buffer = await streamToBuffer(stream); diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts index 4c18b2bea7..380f6323d8 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts @@ -8,12 +8,10 @@ import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-acc import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { UploadProfilePicturePermissionGuard } from 'src/engine/core-modules/user-workspace/guards/upload-profile-picture-permission.guard'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; @@ -21,8 +19,8 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { RoleValidationModule } from 'src/engine/metadata-modules/role-validation/role-validation.module'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; +import { RoleValidationModule } from 'src/engine/metadata-modules/role-validation/role-validation.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @@ -47,7 +45,6 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works WorkspaceDomainsModule, TwentyORMModule, UserRoleModule, - FileUploadModule, FileModule, TokenModule, PermissionsModule, @@ -58,10 +55,6 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works }), ], exports: [UserWorkspaceService], - providers: [ - UserWorkspaceService, - UserWorkspaceResolver, - UploadProfilePicturePermissionGuard, - ], + providers: [UserWorkspaceService, UploadProfilePicturePermissionGuard], }) export class UserWorkspaceModule {} diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.resolver.ts deleted file mode 100644 index 7076486441..0000000000 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.resolver.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { UseGuards } from '@nestjs/common'; -import { Args, Mutation } from '@nestjs/graphql'; - -import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; -import { FileFolder } from 'twenty-shared/types'; - -import type { FileUpload } from 'graphql-upload/processRequest.mjs'; - -import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; -import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto'; -import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { UploadProfilePicturePermissionGuard } from 'src/engine/core-modules/user-workspace/guards/upload-profile-picture-permission.guard'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { streamToBuffer } from 'src/utils/stream-to-buffer'; - -@MetadataResolver() -export class UserWorkspaceResolver { - constructor(private readonly fileUploadService: FileUploadService) {} - - @Mutation(() => SignedFileDTO) - @UseGuards(WorkspaceAuthGuard, UploadProfilePicturePermissionGuard) - async uploadWorkspaceMemberProfilePictureLegacy( - @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - @Args({ name: 'file', type: () => GraphQLUpload }) - { createReadStream, filename, mimetype }: FileUpload, - ): Promise { - const stream = createReadStream(); - const buffer = await streamToBuffer(stream); - const fileFolder = FileFolder.ProfilePicture; - - const { files } = await this.fileUploadService.uploadImage({ - file: buffer, - filename, - mimeType: mimetype, - fileFolder, - workspaceId, - }); - - if (!files.length) { - throw new Error('Failed to upload profile picture'); - } - - return files[0]; - } -} 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 cb1a0dfd05..3a1982818a 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 @@ -1,7 +1,6 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { FileFolder } from 'twenty-shared/types'; import { type DataSource, type Repository } from 'typeorm'; import { type ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity'; @@ -12,10 +11,6 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; -import { - FileUploadService, - type SignedFilesResult, -} from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -40,8 +35,6 @@ describe('UserWorkspaceService', () => { let approvedAccessDomainService: ApprovedAccessDomainService; let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; let userRoleService: UserRoleService; - let fileService: FileService; - let fileUploadService: FileUploadService; let onboardingService: OnboardingService; beforeEach(async () => { @@ -131,6 +124,10 @@ describe('UserWorkspaceService', () => { provide: FileCorePictureService, useValue: {}, }, + { + provide: FileService, + useValue: {}, + }, { provide: FileStorageService, useValue: { @@ -141,18 +138,6 @@ describe('UserWorkspaceService', () => { provide: LoginTokenService, useValue: {}, }, - { - provide: FileUploadService, - useValue: { - uploadImageFromUrl: jest.fn(), - }, - }, - { - provide: FileService, - useValue: { - copyFileFromWorkspaceToWorkspace: jest.fn(), - }, - }, { provide: OnboardingService, useValue: { @@ -169,7 +154,6 @@ describe('UserWorkspaceService', () => { }).compile(); service = module.get(UserWorkspaceService); - fileService = module.get(FileService); userWorkspaceRepository = module.get( getRepositoryToken(UserWorkspaceEntity), ); @@ -189,7 +173,6 @@ describe('UserWorkspaceService', () => { } as unknown as WorkspaceRepository); userRoleService = module.get(UserRoleService); - fileUploadService = module.get(FileUploadService); onboardingService = module.get(OnboardingService); }); @@ -198,42 +181,6 @@ describe('UserWorkspaceService', () => { }); describe('create', () => { - it("should create a user workspace with a default avatar url if it's an existing user with a user workspace having a default avatar url", async () => { - const userId = 'user-id'; - const workspaceId = 'workspace-id'; - const userWorkspace = { - userId, - workspaceId, - } as UserWorkspaceEntity; - - jest - .spyOn(userWorkspaceRepository, 'create') - .mockReturnValue(userWorkspace); - jest - .spyOn(userWorkspaceRepository, 'save') - .mockResolvedValue(userWorkspace); - jest.spyOn(userWorkspaceRepository, 'findOne').mockResolvedValue({ - defaultAvatarUrl: 'path/to/file', - } as UserWorkspaceEntity); - jest - .spyOn(fileService, 'copyFileFromWorkspaceToWorkspace') - .mockResolvedValue(['', 'path/to', 'copy']); - - const result = await service.create({ - userId, - workspaceId, - isExistingUser: true, - }); - - expect(userWorkspaceRepository.create).toHaveBeenCalledWith({ - userId, - workspaceId, - defaultAvatarUrl: 'path/to/copy', - }); - - expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace); - expect(result).toEqual(userWorkspace); - }); it("should create a user workspace without a default avatar url if it's an existing user without any user workspace having a default avatar url", async () => { const userId = 'user-id'; const workspaceId = 'workspace-id'; @@ -265,89 +212,6 @@ describe('UserWorkspaceService', () => { expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace); expect(result).toEqual(userWorkspace); }); - it("should create a user workspace with a default avatar url if it's a new user with a picture url", async () => { - const userId = 'user-id'; - const workspaceId = 'workspace-id'; - const userWorkspace = { - userId, - workspaceId, - } as UserWorkspaceEntity; - - jest - .spyOn(userWorkspaceRepository, 'create') - .mockReturnValue(userWorkspace); - jest - .spyOn(userWorkspaceRepository, 'save') - .mockResolvedValue(userWorkspace); - - jest.spyOn(fileUploadService, 'uploadImageFromUrl').mockResolvedValue({ - files: [{ path: 'path/to/file', token: 'token' }], - } as SignedFilesResult); - - const result = await service.create({ - userId, - workspaceId, - isExistingUser: false, - pictureUrl: 'picture-url', - }); - - expect(fileUploadService.uploadImageFromUrl).toHaveBeenCalledWith({ - imageUrl: 'picture-url', - fileFolder: FileFolder.ProfilePicture, - workspaceId, - }); - expect(userWorkspaceRepository.create).toHaveBeenCalledWith({ - userId, - workspaceId, - defaultAvatarUrl: 'path/to/file', - }); - expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace); - expect(result).toEqual(userWorkspace); - }); - it('should create a user workspace without a default avatar url if image fetch fails', async () => { - const userId = 'user-id'; - const workspaceId = 'workspace-id'; - const userWorkspace = { - userId, - workspaceId, - } as UserWorkspaceEntity; - - jest - .spyOn(userWorkspaceRepository, 'create') - .mockReturnValue(userWorkspace); - jest - .spyOn(userWorkspaceRepository, 'save') - .mockResolvedValue(userWorkspace); - - jest - .spyOn(fileUploadService, 'uploadImageFromUrl') - .mockRejectedValue( - new Error( - 'Failed to fetch image from https://lh3.googleusercontent.com/a/invalid: Request failed with status code 404', - ), - ); - - const result = await service.create({ - userId, - workspaceId, - isExistingUser: false, - pictureUrl: 'https://lh3.googleusercontent.com/a/invalid', - }); - - expect(fileUploadService.uploadImageFromUrl).toHaveBeenCalledTimes(1); - expect(fileUploadService.uploadImageFromUrl).toHaveBeenCalledWith({ - imageUrl: 'https://lh3.googleusercontent.com/a/invalid', - fileFolder: FileFolder.ProfilePicture, - workspaceId, - }); - expect(userWorkspaceRepository.create).toHaveBeenCalledWith({ - userId, - workspaceId, - defaultAvatarUrl: undefined, - }); - expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace); - expect(result).toEqual(userWorkspace); - }); it("should create a user workspace without a default avatar url if it's a new user without a picture url", async () => { const userId = 'user-id'; const workspaceId = 'workspace-id'; @@ -373,45 +237,6 @@ describe('UserWorkspaceService', () => { expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace); expect(result).toEqual(userWorkspace); }); - - it("should create a user workspace without a default avatar url if it's a new user with an empty picture url", async () => { - const userId = 'user-id'; - const workspaceId = 'workspace-id'; - const userWorkspace = { - userId, - workspaceId, - } as unknown as UserWorkspaceEntity; - - jest - .spyOn(userWorkspaceRepository, 'create') - .mockReturnValue(userWorkspace); - jest - .spyOn(userWorkspaceRepository, 'save') - .mockResolvedValue(userWorkspace); - - const uploadImageFromUrlSpy = jest - .spyOn(fileUploadService, 'uploadImageFromUrl') - .mockResolvedValue({ - files: [{ path: 'path/to/file', token: 'token' }], - } as SignedFilesResult); - - const result = await service.create({ - userId, - workspaceId, - isExistingUser: false, - pictureUrl: '', - }); - - expect(uploadImageFromUrlSpy).not.toHaveBeenCalled(); - - expect(userWorkspaceRepository.create).toHaveBeenCalledWith({ - userId, - workspaceId, - defaultAvatarUrl: undefined, - }); - expect(userWorkspaceRepository.save).toHaveBeenCalledWith(userWorkspace); - expect(result).toEqual(userWorkspace); - }); }); describe('createWorkspaceMember', () => { 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 1ca67af181..24fc74356a 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 @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { IsNull, Not, type QueryRunner, type Repository } from 'typeorm'; @@ -18,9 +18,7 @@ import { import { type AvailableWorkspace } from 'src/engine/core-modules/auth/dto/available-workspaces.dto'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; -import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; @@ -62,10 +60,8 @@ export class UserWorkspaceService extends TypeOrmQueryService { const request = getRequest(ctx); @@ -94,7 +88,6 @@ export class WorkspaceResolver { private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly userWorkspaceService: UserWorkspaceService, private readonly twentyConfigService: TwentyConfigService, - private readonly fileUploadService: FileUploadService, private readonly fileService: FileService, private readonly fileUrlService: FileUrlService, private readonly billingSubscriptionService: BillingSubscriptionService, @@ -155,39 +148,6 @@ export class WorkspaceResolver { return 'auto'; } - @Mutation(() => SignedFileDTO) - @UseGuards( - WorkspaceAuthGuard, - SettingsPermissionGuard(PermissionFlagType.WORKSPACE), - ) - async uploadWorkspaceLogoLegacy( - @AuthWorkspace() { id }: WorkspaceEntity, - @Args({ name: 'file', type: () => GraphQLUpload }) - { createReadStream, filename, mimetype }: FileUpload, - ): Promise { - const stream = createReadStream(); - const buffer = await streamToBuffer(stream); - const fileFolder = FileFolder.WorkspaceLogo; - - const { files } = await this.fileUploadService.uploadImage({ - file: buffer, - filename, - mimeType: mimetype, - fileFolder, - workspaceId: id, - }); - - if (!files.length) { - throw new Error('Failed to upload workspace logo'); - } - - await this.workspaceService.updateOne(id, { - logo: files[0].path, - }); - - return files[0]; - } - @ResolveField(() => [FeatureFlagDTO], { nullable: true }) async featureFlags( @Parent() workspace: WorkspaceEntity, @@ -330,35 +290,15 @@ export class WorkspaceResolver { @ResolveField(() => String) async logo(@Parent() workspace: WorkspaceEntity): Promise { - if ( - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - workspace.id, - ) - ) { - if (!isDefined(workspace.logoFileId)) { - return ''; - } - - return this.fileUrlService.signFileByIdUrl({ - fileId: workspace.logoFileId, - workspaceId: workspace.id, - fileFolder: FileFolder.CorePicture, - }); + if (!isDefined(workspace.logoFileId)) { + return ''; } - if (workspace.logo) { - try { - return this.fileService.signFileUrl({ - url: workspace.logo, - workspaceId: workspace.id, - }); - } catch { - return workspace.logo; - } - } - - return workspace.logo ?? ''; + return this.fileUrlService.signFileByIdUrl({ + fileId: workspace.logoFileId, + workspaceId: workspace.id, + fileFolder: FileFolder.CorePicture, + }); } @ResolveField(() => [BillingEntitlementDTO]) diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module.ts index 56f51891e9..93ec1d7f84 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module.ts @@ -2,13 +2,14 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; +import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module'; import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module'; import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -17,6 +18,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache import { AgentMessagePartEntity } from './entities/agent-message-part.entity'; import { AgentMessageEntity } from './entities/agent-message.entity'; import { AgentTurnEntity } from './entities/agent-turn.entity'; +import { AgentMessagePartResolver } from './resolvers/agent-message-part.resolver'; import { AgentActorContextService } from './services/agent-actor-context.service'; import { AgentAsyncExecutorService } from './services/agent-async-executor.service'; @@ -25,6 +27,7 @@ import { AgentAsyncExecutorService } from './services/agent-async-executor.servi AiBillingModule, AiModelsModule, AiAgentModule, + FileUrlModule, WorkspaceDomainsModule, UserWorkspaceModule, UserRoleModule, @@ -40,7 +43,11 @@ import { AgentAsyncExecutorService } from './services/agent-async-executor.servi WorkspaceEntity, ]), ], - providers: [AgentAsyncExecutorService, AgentActorContextService], + providers: [ + AgentAsyncExecutorService, + AgentActorContextService, + AgentMessagePartResolver, + ], exports: [ AgentAsyncExecutorService, AgentActorContextService, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message-part.dto.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message-part.dto.ts index 67beb85e80..751d9195f7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message-part.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message-part.dto.ts @@ -73,6 +73,9 @@ export class AgentMessagePartDTO { @Field(() => String, { nullable: true }) fileFilename: string | null; + @Field(() => UUIDScalarType, { nullable: true }) + fileId: string | null; + @Field(() => String, { nullable: true }) fileUrl: string | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts index 5bf0f78343..a8f65f92c6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity.ts @@ -10,6 +10,7 @@ import { Relation, } from 'typeorm'; +import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity'; @Entity('agentMessagePart') @@ -81,14 +82,18 @@ export class AgentMessagePartEntity { @Column({ type: 'varchar', nullable: true }) sourceDocumentFilename: string | null; - @Column({ type: 'varchar', nullable: true }) - fileMediaType: string | null; - @Column({ type: 'varchar', nullable: true }) fileFilename: string | null; - @Column({ type: 'varchar', nullable: true }) - fileUrl: string | null; + @Column({ type: 'uuid', nullable: true }) + fileId: string | null; + + @ManyToOne(() => FileEntity, { + onDelete: 'RESTRICT', + nullable: true, + }) + @JoinColumn({ name: 'fileId' }) + file: Relation | null; @Column({ type: 'jsonb', nullable: true }) providerMetadata: Record> | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/resolvers/agent-message-part.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/resolvers/agent-message-part.resolver.ts new file mode 100644 index 0000000000..3f32416232 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/resolvers/agent-message-part.resolver.ts @@ -0,0 +1,35 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; + +import { FileFolder } from 'twenty-shared/types'; + +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { AgentMessagePartDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/agent-message-part.dto'; +import { AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity'; + +@Resolver(() => AgentMessagePartDTO) +export class AgentMessagePartResolver { + constructor(private readonly fileUrlService: FileUrlService) {} + + @ResolveField(() => String, { nullable: true }) + fileUrl( + @Parent() part: AgentMessagePartEntity, + @AuthWorkspace() workspace: WorkspaceEntity, + ): string | null { + if (!part.fileId) { + return null; + } + + return this.fileUrlService.signFileByIdUrl({ + fileId: part.fileId, + workspaceId: workspace.id, + fileFolder: FileFolder.AgentChat, + }); + } + + @ResolveField(() => String, { nullable: true }) + fileMediaType(@Parent() part: AgentMessagePartEntity): string | null { + return part.file?.mimeType ?? null; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts index 339e2b9ac3..b632a3351d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/utils/mapUIMessagePartsToDBParts.ts @@ -1,5 +1,8 @@ import { type ToolUIPart } from 'ai'; -import { type ExtendedUIMessagePart } from 'twenty-shared/ai'; +import { + isExtendedFileUIPart, + type ExtendedUIMessagePart, +} from 'twenty-shared/ai'; import { type AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity'; @@ -30,13 +33,17 @@ export const mapUIMessagePartsToDBParts = ( ...basePart, reasoningContent: part.text, }; - case 'file': + case 'file': { + if (!isExtendedFileUIPart(part)) { + throw new Error('Expected file part'); + } + return { ...basePart, - fileMediaType: part.mediaType, fileFilename: part.filename, - fileUrl: part.url, + fileId: part.fileId, }; + } case 'source-url': return { ...basePart, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts index a0ea7e492b..0991aadcd2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/ai-agent.module.ts @@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module'; @@ -33,7 +32,6 @@ import { AgentEntity } from './entities/agent.entity'; ThrottlerModule, AuditModule, FeatureFlagModule, - FileUploadModule, FileModule, ObjectMetadataModule, PermissionsModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts index fb8cfd0ecf..6e4c20f4ce 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/ai-chat.module.ts @@ -14,7 +14,6 @@ import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module'; @@ -83,7 +82,6 @@ import { SystemPromptBuilderService } from './services/system-prompt-builder.ser BillingModule, ThrottlerModule, FeatureFlagModule, - FileUploadModule, FileModule, PermissionsModule, SkillModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts index d712777205..2e19526343 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts @@ -68,15 +68,18 @@ export class AgentChatStreamingService { // Fire user-message save without awaiting to avoid delaying time-to-first-letter. // The promise is awaited inside onFinish where we need the turnId. + const lastUserMessage = messages[messages.length - 1]; const lastUserText = - messages[messages.length - 1]?.parts.find((part) => part.type === 'text') - ?.text ?? ''; + lastUserMessage?.parts.find((part) => part.type === 'text')?.text ?? ''; const userMessagePromise = this.agentChatService.addMessage({ threadId: thread.id, uiMessage: { role: AgentMessageRole.USER, - parts: [{ type: 'text', text: lastUserText }], + parts: + lastUserMessage?.parts.filter( + (part) => part.type === 'text' || part.type === 'file', + ) ?? [], }, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts index d612ece485..da795ef610 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat.service.ts @@ -125,7 +125,7 @@ export class AgentChatService { return this.messageRepository.find({ where: { threadId }, order: { createdAt: 'ASC' }, - relations: ['parts'], + relations: ['parts', 'parts.file'], }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts index 1594d41061..e5f10f57ea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts @@ -18,8 +18,8 @@ import { getAppPath, isDefined } from 'twenty-shared/utils'; import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; -import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const'; import { wrapToolsWithOutputSerialization } from 'src/engine/core-modules/tool-provider/output-serialization/wrap-tools-with-output-serialization.util'; import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; @@ -175,8 +175,7 @@ export class ChatExecutionService { let storedFiles: Array<{ filename: string; - storagePath: string; - url: string; + fileId: string; }> = []; if (extractedFiles.length > 0) { @@ -372,13 +371,10 @@ export class ChatExecutionService { private async storeExtractedFiles( files: ExtractedFile[], _workspaceId: string, - ): Promise> { - // Files are already uploaded and have URLs, just return them with their info - // The code interpreter tool will download them when needed + ): Promise> { return files.map((file) => ({ filename: file.filename, - storagePath: file.filename, - url: file.url, + fileId: file.fileId, })); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts index 03cc06f66d..f132187183 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts @@ -3,12 +3,12 @@ import { Injectable } from '@nestjs/common'; import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const'; import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum'; import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service'; -import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { EXECUTE_TOOL_TOOL_NAME, LEARN_TOOLS_TOOL_NAME, LOAD_SKILL_TOOL_NAME, } from 'src/engine/core-modules/tool-provider/tools'; +import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { AgentActorContextService, type UserContext, @@ -137,8 +137,7 @@ export class SystemPromptBuilderService { contextString?: string, storedFiles?: Array<{ filename: string; - storagePath: string; - url: string; + fileId: string; }>, workspaceInstructions?: string, userContext?: UserContext, @@ -198,12 +197,12 @@ ${parts.join('\n')}`; } buildUploadedFilesSection( - storedFiles: Array<{ filename: string; storagePath: string; url: string }>, + storedFiles: Array<{ filename: string; fileId: string }>, ): string { const fileList = storedFiles.map((f) => `- ${f.filename}`).join('\n'); const filesJson = JSON.stringify( - storedFiles.map((f) => ({ filename: f.filename, url: f.url })), + storedFiles.map((f) => ({ filename: f.filename, fileId: f.fileId })), ); return ` @@ -213,7 +212,7 @@ The user has uploaded the following files: ${fileList} **IMPORTANT**: Use the \`code_interpreter\` tool to analyze these files. -When calling code_interpreter, include the files parameter with these values: +When calling code_interpreter, include the files parameter with these values (use fileId to reference uploaded files): \`\`\`json ${filesJson} \`\`\` diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts index 0d49c8cef4..fb354332c0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts @@ -1,4 +1,5 @@ import { type UIMessage } from 'ai'; +import { isExtendedFileUIPart } from 'twenty-shared/ai'; const CODE_INTERPRETER_MIME_TYPES = new Set([ 'text/csv', @@ -18,7 +19,7 @@ const CODE_INTERPRETER_MIME_TYPES = new Set([ export type ExtractedFile = { filename: string; - url: string; + fileId: string; mimeType: string; }; @@ -41,13 +42,13 @@ export const extractCodeInterpreterFiles = ( const filesForThisMessage: ExtractedFile[] = []; for (const part of message.parts) { - if (part.type === 'file') { + if (isExtendedFileUIPart(part)) { const mimeType = part.mediaType ?? ''; if (CODE_INTERPRETER_MIME_TYPES.has(mimeType)) { filesForThisMessage.push({ filename: part.filename ?? 'uploaded_file', - url: part.url, + fileId: part.fileId, mimeType, }); } else { diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts index ddc365ca7b..f52746eb4a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.module.ts @@ -9,7 +9,6 @@ import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { LogicFunctionResourceModule } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.module'; import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; @@ -26,7 +25,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace @Module({ imports: [ - FileUploadModule, NestjsQueryTypeOrmModule.forFeature([LogicFunctionEntity]), TypeOrmModule.forFeature([ApplicationEntity, FeatureFlagEntity]), ThrottlerModule, diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index b03497a8a7..193aed86d0 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -245,10 +245,7 @@ describe('WorkspaceEntityManager', () => { IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false, IS_MARKETPLACE_ENABLED: false, IS_DATE_TIME_WHOLE_DAY_FILTER_ENABLED: false, - IS_FILES_FIELD_MIGRATED: false, IS_DRAFT_EMAIL_ENABLED: false, - IS_CORE_PICTURE_MIGRATED: false, - IS_OTHER_FILE_MIGRATED: false, }, userWorkspaceRoleMap: {}, eventEmitterService: { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index 2a2f930506..1463c353cc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -105,21 +105,6 @@ export const seedFeatureFlags = async ({ workspaceId: workspaceId, value: true, }, - { - key: FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - workspaceId: workspaceId, - value: true, - }, - { - key: FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - workspaceId: workspaceId, - value: true, - }, - { - key: FeatureFlagKey.IS_OTHER_FILE_MIGRATED, - workspaceId: workspaceId, - value: true, - }, ]) .execute(); }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts index 9360fb3594..ca83586018 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/constant/default-feature-flags.ts @@ -4,7 +4,4 @@ export const DEFAULT_FEATURE_FLAGS = [ FeatureFlagKey.IS_ATTACHMENT_MIGRATED, FeatureFlagKey.IS_NOTE_TARGET_MIGRATED, FeatureFlagKey.IS_TASK_TARGET_MIGRATED, - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - FeatureFlagKey.IS_FILES_FIELD_MIGRATED, - FeatureFlagKey.IS_OTHER_FILE_MIGRATED, ] as const satisfies FeatureFlagKey[]; diff --git a/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts b/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts index f43e68c41d..3ffb262534 100644 --- a/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts +++ b/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts @@ -5,12 +5,11 @@ import { ObjectRecordDestroyEvent, ObjectRecordUpdateEvent, } from 'twenty-shared/database-events'; -import { FileFolder, FeatureFlagKey } from 'twenty-shared/types'; +import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; @@ -19,7 +18,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class WorkspaceMemberAvatarFileDeletionListener { constructor( - private readonly featureFlagService: FeatureFlagService, private readonly fileCorePictureService: FileCorePictureService, ) {} @@ -29,15 +27,6 @@ export class WorkspaceMemberAvatarFileDeletionListener { ObjectRecordUpdateEvent >, ) { - if ( - !(await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_CORE_PICTURE_MIGRATED, - payload.workspaceId, - )) - ) { - return; - } - const fileIdsToDelete = this.getFileIdsToDeleteFromUpdateEvent(payload); this.deleteCorePictures(fileIdsToDelete, payload.workspaceId); diff --git a/packages/twenty-shared/src/ai/index.ts b/packages/twenty-shared/src/ai/index.ts index f5c57edc34..55fe5fa9af 100644 --- a/packages/twenty-shared/src/ai/index.ts +++ b/packages/twenty-shared/src/ai/index.ts @@ -13,10 +13,12 @@ export type { } from './types/agent-response-schema.type'; export type { CodeExecutionFile, + ExtendedFileUIPart, CodeExecutionState, CodeExecutionData, DataMessagePart, } from './types/DataMessagePart'; +export { isExtendedFileUIPart } from './types/DataMessagePart'; export type { AIChatUsageMetadata, AIChatModelMetadata, diff --git a/packages/twenty-shared/src/ai/types/DataMessagePart.ts b/packages/twenty-shared/src/ai/types/DataMessagePart.ts index d8ca40444b..45f5812434 100644 --- a/packages/twenty-shared/src/ai/types/DataMessagePart.ts +++ b/packages/twenty-shared/src/ai/types/DataMessagePart.ts @@ -1,9 +1,31 @@ +import { isDefined } from '@/utils/validation/isDefined'; + export type CodeExecutionFile = { + fileId: string; filename: string; url: string; mimeType: string; }; +export type ExtendedFileUIPart = { + type: 'file'; + mediaType: string; + filename?: string; + url: string; + fileId: string; +}; + +export const isExtendedFileUIPart = ( + part: Record, +): part is ExtendedFileUIPart => { + return ( + part.type === 'file' && + isDefined(part.fileId) && + isDefined(part.url) && + isDefined(part.mediaType) + ); +}; + export type CodeExecutionState = 'pending' | 'running' | 'completed' | 'error'; export type CodeExecutionData = { diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts index e0c831e23f..26595633b5 100644 --- a/packages/twenty-shared/src/types/FeatureFlagKey.ts +++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts @@ -12,9 +12,6 @@ export enum FeatureFlagKey { IS_ATTACHMENT_MIGRATED = 'IS_ATTACHMENT_MIGRATED', IS_NOTE_TARGET_MIGRATED = 'IS_NOTE_TARGET_MIGRATED', IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED', - IS_FILES_FIELD_MIGRATED = 'IS_FILES_FIELD_MIGRATED', - IS_CORE_PICTURE_MIGRATED = 'IS_CORE_PICTURE_MIGRATED', - IS_OTHER_FILE_MIGRATED = 'IS_OTHER_FILE_MIGRATED', IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', IS_COMMAND_MENU_ITEM_ENABLED = 'IS_COMMAND_MENU_ITEM_ENABLED',