Skip to content

Files - Migrate attachments in activities#17808

Merged
etiennejouan merged 17 commits intomainfrom
ej/rich-text-migration
Feb 13, 2026
Merged

Files - Migrate attachments in activities#17808
etiennejouan merged 17 commits intomainfrom
ej/rich-text-migration

Conversation

@etiennejouan
Copy link
Copy Markdown
Contributor

@etiennejouan etiennejouan commented Feb 9, 2026

As attachment files have migrated from fullPath to file files field, need to migrate richText logic to fit to new attachment file handling + data migration

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR migrates activity (note/task) rich-text attachments toward the new Files Field model by:

  • Adding an attachmentFileId prop to Blocknote file blocks and wiring upload to return a signed /files-field/... URL + fileId.
  • Updating attachment-sync utilities to compare URLs correctly in both legacy (fullPath) and files-field (attachment.file[0].url) modes.
  • Extending the server-side activity query result handler to sign /files-field URLs into rich-text blocks when attachmentFileId is present.
  • Introducing a 1.18 upgrade command that backfills attachmentFileId into existing rich-text blocks (and creates the Attachment file field metadata if missing).

Key issues to fix before merge:

  • Frontend attachment rows can render/call download with an undefined URL when files-field is enabled but attachment.file is absent.
  • The 1.18 migration command’s legacy URL→storage path normalization drops the token subfolder, which will cause file copy/backfill to fail for typical legacy attachment layouts.

Confidence Score: 3/5

  • This PR is close to merge-ready but has a couple of runtime/data-migration issues that should be addressed first.
  • Most changes are straightforward plumbing for the Files Field migration, but there are two concrete problems: (1) the UI can use an undefined URL for migrated attachments when attachment.file is missing, and (2) the new 1.18 backfill command likely computes the wrong legacy storage path, causing file copy/backfill to fail for common legacy URLs.
  • packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx; packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts

Important Files Changed

Filename Overview
packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx Adds URL→fileId mapping to enrich blocknote payloads with attachmentFileId and record them on upload/initial parse.
packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx Switches to files-field URL/source fields for download/open; currently can pass undefined URL when file data missing (bug).
packages/twenty-front/src/modules/ui/input/editor/hooks/useAttachmentSync.ts Plumbs isFilesFieldMigrated flag through attachment sync utilities.
packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts Adds 1.18 migration command to enrich rich-text blocks with attachmentFileId; legacy path parsing likely drops token subfolder causing copy failures.
packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts When files-field migrated and attachmentFileId is present, signs files-field URL into block props.url.
packages/twenty-server/src/engine/core-modules/file/files-field/files-field.service.ts Changes uploadFile return type to FilesFieldFileDTO and includes signed url.

Sequence Diagram

sequenceDiagram
  participant FE as Frontend (ActivityRichTextEditor)
  participant Hook as useUploadAttachmentFile
  participant GQL as GraphQL API (uploadFilesFieldFile)
  participant FF as FeatureFlagService
  participant FFS as FilesFieldService
  participant DB as DB/File storage

  FE->>Hook: uploadAttachmentFile(file, target)
  Hook->>FF: isFeatureEnabled(IS_FILES_FIELD_MIGRATED)
  alt files-field migrated
    Hook->>GQL: uploadFilesFieldFile(fieldMetadataId, file)
    GQL->>FFS: uploadFile(buffer, filename, ...)
    FFS->>DB: writeFile_v2 + save FileEntity
    FFS-->>GQL: FilesFieldFileDTO{id,path,size,createdAt,url}
    Hook-->>FE: {attachmentAbsoluteURL: url, attachmentFileId: id}
    FE->>FE: urlToFileIdMap.set(url, id)
    FE->>FE: Persist body enriched with attachmentFileId
  else legacy
    Hook->>GQL: uploadFile (legacy)
    Hook-->>FE: {attachmentAbsoluteURL: fullPath}
  end

  Note over FE: Later reads of activity body
  FE->>GQL: query note/task
  GQL->>FF: isFeatureEnabled(IS_FILES_FIELD_MIGRATED)
  alt migrated and attachmentFileId present
    GQL->>FFS: signFileUrl(fileId, workspaceId)
    GQL-->>FE: block props.url replaced with signed /files-field URL
  else legacy image
    GQL->>DB: sign legacy /files/attachment URL
    GQL-->>FE: block props.url replaced with signed /files URL
  end
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 9, 2026

Additional Comments (1)

packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx
Undefined file URL

When IS_FILES_FIELD_MIGRATED is enabled and attachment.file is null/empty (allowed by Attachment.file?: FieldFilesValue[] | null), fileUrl becomes undefined (forced via as string) and then gets used in href={fileUrl} and downloadFile(fileUrl, ...), which will break opening/downloading attachments at runtime. This needs a real guard (or a guaranteed non-null URL) before rendering/using the link.

Context Used: Context from dashboard - When using short-circuit evaluation for rendering, ensure that it does not lead to rendering issues.... (source)

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 29 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx">

<violation number="1" location="packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx:107">
P2: Force-casting the migrated file URL to string removes the previous runtime guard, so `fileUrl` can be undefined and be used in `href`/`downloadFile`, leading to broken links or errors when `attachment.file` is empty. Keep a runtime check or fallback for the migrated case.</violation>
</file>

<file name="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts">

<violation number="1" location="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts:307">
P1: `findMatchingFileId` can throw from `getAttachmentLegacyRelativePath`/`getAttachmentNewdRelativePath` if the URL doesn't match expected patterns. This is called without try/catch in the migration loop, so a single unexpected URL format (e.g., an external URL) will crash the migration for the entire workspace. Wrap the call in a try/catch or add a guard to skip unrecognized URL formats.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 9, 2026

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:39202

This environment will automatically shut down when the PR is closed or after 5 hours.

@etiennejouan etiennejouan force-pushed the ej/rich-text-migration branch from 1b6a200 to fae6b68 Compare February 9, 2026 15:42
Copy link
Copy Markdown
Contributor

@prastoin prastoin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend review
Main points:

  • enrichment done server side
  • field file settings minimalFileCount

Copy link
Copy Markdown
Contributor

@prastoin prastoin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will re-review backend when you've implemented the refactor we've been discussing about !

@etiennejouan etiennejouan force-pushed the ej/rich-text-migration branch from e41220f to ec81481 Compare February 12, 2026 13:56
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts">

<violation number="1" location="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts:212">
P2: Avoid loading full activity rows when only id/bodyV2 are needed; this increases memory and DB load for large workspaces.</violation>

<violation number="2" location="packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts:351">
P2: Map keys should be normalized to the same relative path format used in findMatchingFileId; storing raw fullPath will prevent matches and can create duplicate attachments.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

`Migrating ${activityType} rich text for workspace ${workspaceId}`,
);

const activities = await activityRepository.find();
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid loading full activity rows when only id/bodyV2 are needed; this increases memory and DB load for large workspaces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts, line 212:

<comment>Avoid loading full activity rows when only id/bodyV2 are needed; this increases memory and DB load for large workspaces.</comment>

<file context>
@@ -209,9 +209,7 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
-    const activities = await activityRepository.find({
-      select: ['id', 'bodyV2'],
-    });
+    const activities = await activityRepository.find();
 
     this.logger.log(
</file context>
Suggested change
const activities = await activityRepository.find();
const activities = await activityRepository.find({
select: ['id', 'bodyV2'],
});
Fix with Cubic

const fileData = attachment.file[0];

if (isDefined(attachment.fullPath) && isDefined(fileData.fileId)) {
urlToFileIdMap.set(attachment.fullPath, fileData.fileId);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Map keys should be normalized to the same relative path format used in findMatchingFileId; storing raw fullPath will prevent matches and can create duplicate attachments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command.ts, line 351:

<comment>Map keys should be normalized to the same relative path format used in findMatchingFileId; storing raw fullPath will prevent matches and can create duplicate attachments.</comment>

<file context>
@@ -350,11 +348,7 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
-          );
-
-          urlToFileIdMap.set(normalizedPath, fileData.fileId);
+          urlToFileIdMap.set(attachment.fullPath, fileData.fileId);
         }
       }
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts">

<violation number="1" location="packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts:176">
P2: The new spy uses `mockResolvedValue(false)` which persists across tests because `afterEach` only calls `jest.clearAllMocks()` (it does not restore spy implementations). This can unintentionally force the feature flag off in later tests. Prefer a one-time mock to keep the change scoped to this test.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

it('when image block has an internal attachment URL (legacy path)', async () => {
jest
.spyOn(mockFeatureFlagService, 'isFeatureEnabled')
.mockResolvedValue(false);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new spy uses mockResolvedValue(false) which persists across tests because afterEach only calls jest.clearAllMocks() (it does not restore spy implementations). This can unintentionally force the feature flag off in later tests. Prefer a one-time mock to keep the change scoped to this test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-v2-field-query-result-getter.handler.spec.ts, line 176:

<comment>The new spy uses `mockResolvedValue(false)` which persists across tests because `afterEach` only calls `jest.clearAllMocks()` (it does not restore spy implementations). This can unintentionally force the feature flag off in later tests. Prefer a one-time mock to keep the change scoped to this test.</comment>

<file context>
@@ -170,7 +170,11 @@ describe('RichTextV2FieldQueryResultGetterHandler', () => {
+    it('when image block has an internal attachment URL (legacy path)', async () => {
+      jest
+        .spyOn(mockFeatureFlagService, 'isFeatureEnabled')
+        .mockResolvedValue(false);
+
       const imageBlock = {
</file context>
Suggested change
.mockResolvedValue(false);
.mockResolvedValueOnce(false);
Fix with Cubic

}

const pathname = parsedUrl.pathname;
const isLinkExternal = !pathname.startsWith('/files-field/');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remark: Not sure about that, why wouldn't we be looking to the domain ? I'm pretty sure there's a reason, do you have any information @charlesBochet ?

let fileId = this.findMatchingFileId(url, urlToFileIdMap);

if (!isDefined(fileId) && !isDryRun) {
const createdFileId = await this.createAttachmentFromUrl(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remark: if an operation file during a blocknote migration we would have some standalone files, nothing critical though

@prastoin
Copy link
Copy Markdown
Contributor

image

@etiennejouan etiennejouan added this pull request to the merge queue Feb 13, 2026
Merged via the queue into main with commit 5c2c588 Feb 13, 2026
86 of 87 checks passed
@etiennejouan etiennejouan deleted the ej/rich-text-migration branch February 13, 2026 09:29
@twenty-eng-sync
Copy link
Copy Markdown

Hey @etiennejouan! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

1 similar comment
@twenty-eng-sync
Copy link
Copy Markdown

Hey @etiennejouan! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants