Add email thread widget and message thread record page layout (#19351)

## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach

## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings

## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-06 12:02:04 +02:00
committed by GitHub
parent 4aa1d71b12
commit 8acfacc69c
56 changed files with 2441 additions and 2371 deletions
@@ -0,0 +1,73 @@
import { Command } from 'nest-commander';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
@Command({
name: 'upgrade:1-21:backfill-message-thread-subject',
description:
'Backfill messageThread.subject from the most recently received message in each thread',
})
export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
dataSource,
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (!dataSource) {
this.logger.log(`No data source for workspace ${workspaceId}, skipping`);
return;
}
const schemaName = getWorkspaceSchemaName(workspaceId);
if (options.dryRun) {
this.logger.log(
`[DRY RUN] Would backfill messageThread.subject for workspace ${workspaceId}`,
);
return;
}
const columnExists = await dataSource.query(
`SELECT 1 FROM information_schema.columns
WHERE table_schema = $1
AND table_name = 'messageThread'
AND column_name = 'subject'`,
[schemaName],
);
if (columnExists.length === 0) {
this.logger.log(
`Column "subject" does not exist yet on messageThread for workspace ${workspaceId}, skipping (will be created by sync-metadata)`,
);
return;
}
const result = await dataSource.query(
`UPDATE "${schemaName}"."messageThread" mt
SET "subject" = sub.subject
FROM (
SELECT DISTINCT ON ("messageThreadId") "messageThreadId", "subject"
FROM "${schemaName}"."message"
ORDER BY "messageThreadId", "receivedAt" DESC NULLS LAST
) sub
WHERE mt.id = sub."messageThreadId"
AND mt."subject" IS NULL`,
);
this.logger.log(
`Backfilled subject for ${result?.[1] ?? 0} message threads in workspace ${workspaceId}`,
);
}
}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-message-thread-subject.command';
import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-add-global-key-value-pair-unique-index.command';
import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-datasource-to-workspace.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command';
@@ -39,6 +40,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
providers: [
AddGlobalKeyValuePairUniqueIndexCommand,
BackfillDatasourceToWorkspaceCommand,
BackfillMessageThreadSubjectCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
DeduplicateEngineCommandsCommand,
FixSelectAllCommandMenuItemsCommand,
@@ -50,6 +52,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
exports: [
AddGlobalKeyValuePairUniqueIndexCommand,
BackfillDatasourceToWorkspaceCommand,
BackfillMessageThreadSubjectCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
DeduplicateEngineCommandsCommand,
FixSelectAllCommandMenuItemsCommand,
@@ -28,6 +28,7 @@ import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upg
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
import { AddGlobalKeyValuePairUniqueIndexCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-add-global-key-value-pair-unique-index.command';
import { BackfillDatasourceToWorkspaceCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-datasource-to-workspace.command';
import { BackfillMessageThreadSubjectCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-message-thread-subject.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-backfill-page-layouts-and-fields-widget-view-fields.command';
import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command';
import { FixSelectAllCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-fix-select-all-command-menu-items.command';
@@ -75,6 +76,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 1.21 Commands
private readonly addGlobalKeyValuePairUniqueIndexCommand: AddGlobalKeyValuePairUniqueIndexCommand,
private readonly backfillDatasourceToWorkspaceCommand: BackfillDatasourceToWorkspaceCommand,
private readonly backfillMessageThreadSubjectCommand: BackfillMessageThreadSubjectCommand,
private readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
private readonly deduplicateEngineCommandsCommand: DeduplicateEngineCommandsCommand,
private readonly fixSelectAllCommandMenuItemsCommand: FixSelectAllCommandMenuItemsCommand,
@@ -116,6 +118,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
const commands_1210: VersionCommands = [
this.addGlobalKeyValuePairUniqueIndexCommand,
this.backfillDatasourceToWorkspaceCommand,
this.backfillMessageThreadSubjectCommand,
this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
this.deduplicateEngineCommandsCommand,
this.fixSelectAllCommandMenuItemsCommand,