message list member backfill (#23176)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23176?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
neo773
2026-07-24 00:55:46 +05:30
committed by GitHub
parent 97d2b52a71
commit 59eead238d
5 changed files with 160 additions and 7 deletions
@@ -7,8 +7,7 @@ import {
type UnsubscribeTopicsQuery,
UnsubscribeTopicVisibility,
} from '~/generated-metadata/graphql';
import { Pill, Status } from 'twenty-ui/data-display';
import { IconLock } from 'twenty-ui/icon';
import { Status } from 'twenty-ui/data-display';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
type UnsubscribeTopic = UnsubscribeTopicsQuery['unsubscribeTopics'][number];
@@ -20,13 +19,11 @@ export const SettingsWorkspaceUnsubscribeTopicSection = () => {
const title = t`Unsubscribe topics`;
const description = t`Email categories recipients can opt out of`;
const organizationPill = <Pill Icon={IconLock} label={t`Organization`} />;
return (
<SettingsTableListSection<UnsubscribeTopic>
title={title}
description={description}
headerAdornment={organizationPill}
items={unsubscribeTopics}
columns={[
{
@@ -6,8 +6,6 @@ import { SettingsTableListSection } from '@/settings/components/SettingsTableLis
import { SettingsWorkspaceEmailChannelDomainStatusCell } from '@/settings/workspace/components/SettingsWorkspaceEmailChannelDomainStatusCell';
import { SettingsWorkspaceEmailGroupSourceCell } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSourceCell';
import { MessageChannelType, SettingsPath } from 'twenty-shared/types';
import { Pill } from 'twenty-ui/data-display';
import { IconLock } from 'twenty-ui/icon';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const SettingsWorkspaceEmailGroupSection = () => {
@@ -23,7 +21,6 @@ export const SettingsWorkspaceEmailGroupSection = () => {
<SettingsTableListSection<MessageChannel>
title={t`Channels`}
description={t`Addresses your workspace uses to send and receive email from shared inboxes`}
headerAdornment={<Pill Icon={IconLock} label={t`Organization`} />}
items={emailGroupChannels}
columns={[
{ label: t`Email`, Cell: SettingsWorkspaceEmailGroupSourceCell },
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { BackfillMessageListMembersJunctionTargetCommand } from 'src/database/commands/upgrade-version-command/2-24/2-24-workspace-command-1784567000000-backfill-message-list-members-junction-target.command';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
TypeOrmModule.forFeature([FieldMetadataEntity]),
WorkspaceCacheModule,
WorkspaceMigrationRunnerModule,
WorkspaceIteratorModule,
],
providers: [BackfillMessageListMembersJunctionTargetCommand],
})
export class V2_24_UpgradeVersionCommandModule {}
@@ -0,0 +1,138 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-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 { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isFieldMetadataSettingsOfType } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-settings-of-type.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
import { getMetadataSerializedRelationNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
const MEMBERS_FIELD_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.messageList.fields.members.universalIdentifier;
const PERSON_FIELD_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.messageListMember.fields.person.universalIdentifier;
@RegisteredWorkspaceCommand('2.24.0', 1784567000000)
@Command({
name: 'upgrade:2-24:backfill-message-list-members-junction-target',
description:
'Backfill the junction target field id on messageList.members for workspaces provisioned before it was declared, so the record page renders the members junction picker.',
})
export class BackfillMessageListMembersJunctionTargetCommand extends ProvisionedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const { flatFieldMetadataMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatFieldMetadataMaps',
]);
const membersFlatFieldMetadata =
flatFieldMetadataMaps.byUniversalIdentifier[
MEMBERS_FIELD_UNIVERSAL_IDENTIFIER
];
if (!isDefined(membersFlatFieldMetadata)) {
this.logger.log(
`No messageList.members field for workspace ${workspaceId}, skipping`,
);
return;
}
const currentSettings = membersFlatFieldMetadata.settings;
if (
!isFieldMetadataSettingsOfType(currentSettings, FieldMetadataType.RELATION)
) {
this.logger.warn(
`messageList.members has no relation settings for workspace ${workspaceId}, skipping`,
);
return;
}
if (isDefined(currentSettings.junctionTargetFieldId)) {
this.logger.log(
`messageList.members junction target already set for workspace ${workspaceId}, skipping`,
);
return;
}
const personFlatFieldMetadata =
flatFieldMetadataMaps.byUniversalIdentifier[
PERSON_FIELD_UNIVERSAL_IDENTIFIER
];
if (!isDefined(personFlatFieldMetadata)) {
this.logger.warn(
`No messageListMember.person field for workspace ${workspaceId}, skipping`,
);
return;
}
const updatedSettings = {
...currentSettings,
junctionTargetFieldId: personFlatFieldMetadata.id,
};
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Backfilling messageList.members junction target for workspace ${workspaceId}`,
);
if (isDryRun) {
return;
}
await this.fieldMetadataRepository.update(
{ id: membersFlatFieldMetadata.id, workspaceId },
{ settings: updatedSettings },
);
const fieldMetadataRelatedNames = [
'fieldMetadata',
...getMetadataRelatedMetadataNames('fieldMetadata'),
...getMetadataSerializedRelationNames('fieldMetadata'),
'index',
] as const;
const allFlatEntityMapsKeys = [
...new Set(fieldMetadataRelatedNames.map(getMetadataFlatEntityMapsKey)),
];
await this.workspaceMigrationRunnerService.invalidateCache({
allFlatEntityMapsKeys,
workspaceId,
});
this.logger.log(
`Backfilled messageList.members junction target for workspace ${workspaceId}`,
);
}
}
@@ -24,6 +24,7 @@ import { V2_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
import { V2_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-21/2-21-upgrade-version-command.module';
import { V2_22_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-22/2-22-upgrade-version-command.module';
import { V2_23_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-23/2-23-upgrade-version-command.module';
import { V2_24_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-24/2-24-upgrade-version-command.module';
@Module({
imports: [
@@ -51,6 +52,7 @@ import { V2_23_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
V2_21_UpgradeVersionCommandModule,
V2_22_UpgradeVersionCommandModule,
V2_23_UpgradeVersionCommandModule,
V2_24_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}