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:
+10
@@ -1,7 +1,17 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
|
||||
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
|
||||
const SUBJECT_FIELD_NAME = 'subject';
|
||||
|
||||
export const SEARCH_FIELDS_FOR_MESSAGE_THREAD: FieldTypeAndNameMetadata[] = [
|
||||
{ name: SUBJECT_FIELD_NAME, type: FieldMetadataType.TEXT },
|
||||
];
|
||||
|
||||
export class MessageThreadWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
subject: string | null;
|
||||
messages: EntityRelation<MessageWorkspaceEntity[]>;
|
||||
}
|
||||
|
||||
+48
-5
@@ -25,7 +25,7 @@ type MessageAccumulator = {
|
||||
| 'text'
|
||||
| 'messageThreadId'
|
||||
>;
|
||||
threadToCreate?: Pick<MessageThreadWorkspaceEntity, 'id'>;
|
||||
threadToCreate?: Pick<MessageThreadWorkspaceEntity, 'id' | 'subject'>;
|
||||
messageChannelMessageAssociationToCreate?: Pick<
|
||||
MessageChannelMessageAssociationWorkspaceEntity,
|
||||
| 'id'
|
||||
@@ -199,10 +199,52 @@ export class MessagingMessageService {
|
||||
.map((accumulator) => accumulator.threadToCreate)
|
||||
.filter(isDefined);
|
||||
|
||||
await messageThreadRepository.insert(
|
||||
messageThreadsToCreate,
|
||||
transactionManager,
|
||||
);
|
||||
const threadSubjectUpdates = new Map<
|
||||
string,
|
||||
{ subject: string; receivedAt: number }
|
||||
>();
|
||||
|
||||
for (const message of messages) {
|
||||
const messageAccumulator = messageAccumulatorMap.get(
|
||||
message.externalId,
|
||||
);
|
||||
|
||||
if (!isDefined(messageAccumulator)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(messageAccumulator.existingThreadInDB) &&
|
||||
isDefined(messageAccumulator.messageToCreate) &&
|
||||
isDefined(message.subject)
|
||||
) {
|
||||
const threadId = messageAccumulator.existingThreadInDB.id;
|
||||
const existing = threadSubjectUpdates.get(threadId);
|
||||
const receivedAt = message.receivedAt?.getTime() ?? 0;
|
||||
|
||||
if (!isDefined(existing) || receivedAt > existing.receivedAt) {
|
||||
threadSubjectUpdates.set(threadId, {
|
||||
subject: message.subject,
|
||||
receivedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const threadsToUpsert = [
|
||||
...messageThreadsToCreate,
|
||||
...Array.from(threadSubjectUpdates.entries()).map(
|
||||
([id, { subject }]) => ({ id, subject }),
|
||||
),
|
||||
];
|
||||
|
||||
if (threadsToUpsert.length > 0) {
|
||||
await messageThreadRepository.upsert(
|
||||
threadsToUpsert,
|
||||
['id'],
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
const messagesToCreate = Array.from(messageAccumulatorMap.values())
|
||||
.map((accumulator) => accumulator.messageToCreate)
|
||||
@@ -451,6 +493,7 @@ export class MessagingMessageService {
|
||||
|
||||
messageAccumulator.threadToCreate = {
|
||||
id: newOrExistingMessageThreadId,
|
||||
subject: message.subject,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user