Feat/email composer improvements (#23188)
- Move composer to dedicated page - Add test email option - Auto saved as draft can be revisited from `objects/messageCampaigns` later - Campaign stats component https://github.com/user-attachments/assets/9e523116-e79b-496d-9c9d-3887e0c9213f <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23188?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. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.25.0', 1785229950000)
|
||||
export class AddMessageCampaignWidgetTypeFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "core"."pageLayoutWidget_type_enum" RENAME TO "pageLayoutWidget_type_enum_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."pageLayoutWidget_type_enum" AS ENUM('VIEW', 'IFRAME', 'FIELD', 'FIELDS', 'GRAPH', 'STANDALONE_RICH_TEXT', 'TIMELINE', 'TASKS', 'NOTES', 'FILES', 'EMAILS', 'CALENDAR', 'FIELD_RICH_TEXT', 'WORKFLOW', 'WORKFLOW_VERSION', 'WORKFLOW_RUN', 'FRONT_COMPONENT', 'RECORD_TABLE', 'EMAIL_THREAD', 'MESSAGE_CAMPAIGN_BODY', 'MESSAGE_CAMPAIGN_DETAILS')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" TYPE "core"."pageLayoutWidget_type_enum" USING "type"::"text"::"core"."pageLayoutWidget_type_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" SET DEFAULT 'VIEW'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."pageLayoutWidget_type_enum_old"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "core"."pageLayoutWidget" WHERE "type" IN ('MESSAGE_CAMPAIGN_BODY', 'MESSAGE_CAMPAIGN_DETAILS')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."pageLayoutWidget_type_enum_old" AS ENUM('VIEW', 'IFRAME', 'FIELD', 'FIELDS', 'GRAPH', 'STANDALONE_RICH_TEXT', 'TIMELINE', 'TASKS', 'NOTES', 'FILES', 'EMAILS', 'CALENDAR', 'FIELD_RICH_TEXT', 'WORKFLOW', 'WORKFLOW_VERSION', 'WORKFLOW_RUN', 'FRONT_COMPONENT', 'RECORD_TABLE', 'EMAIL_THREAD')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" TYPE "core"."pageLayoutWidget_type_enum_old" USING "type"::"text"::"core"."pageLayoutWidget_type_enum_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."pageLayoutWidget" ALTER COLUMN "type" SET DEFAULT 'VIEW'`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "core"."pageLayoutWidget_type_enum"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "core"."pageLayoutWidget_type_enum_old" RENAME TO "pageLayoutWidget_type_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -3,17 +3,29 @@ 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-25/2-25-workspace-command-1784567000000-backfill-message-list-members-junction-target.command';
|
||||
import { AddMessageCampaignComposerTabCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785229940000-add-message-campaign-composer-tab.command';
|
||||
import { ConfigureMessageCampaignCommandMenuCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785229960000-configure-message-campaign-command-menu.command';
|
||||
import { AddMessageCampaignNameFieldCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-workspace-command-1785229970000-add-message-campaign-name-field.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
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';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity]),
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceIteratorModule,
|
||||
],
|
||||
providers: [BackfillMessageListMembersJunctionTargetCommand],
|
||||
providers: [
|
||||
BackfillMessageListMembersJunctionTargetCommand,
|
||||
AddMessageCampaignComposerTabCommand,
|
||||
ConfigureMessageCampaignCommandMenuCommand,
|
||||
AddMessageCampaignNameFieldCommand,
|
||||
],
|
||||
})
|
||||
export class V2_25_UpgradeVersionCommandModule {}
|
||||
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import {
|
||||
STANDARD_OBJECTS,
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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 { getStandardFlatEntitiesToCreateOrThrow } from 'src/database/commands/upgrade-version-command/2-10/utils/get-standard-flat-entities-to-create-or-throw.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const MESSAGE_CAMPAIGN_RECORD_PAGE_FIELDS_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_OBJECTS.messageCampaign.views.messageCampaignRecordPageFields
|
||||
.universalIdentifier;
|
||||
|
||||
const MESSAGE_CAMPAIGN_RECORD_PAGE_FIELDS_VIEW_FIELD_UNIVERSAL_IDENTIFIERS =
|
||||
Object.values(
|
||||
STANDARD_OBJECTS.messageCampaign.views.messageCampaignRecordPageFields
|
||||
.viewFields,
|
||||
).map((viewField) => viewField.universalIdentifier);
|
||||
|
||||
const MESSAGE_CAMPAIGN_RECORD_PAGE_FIELDS_VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIERS =
|
||||
Object.values(
|
||||
STANDARD_OBJECTS.messageCampaign.views.messageCampaignRecordPageFields
|
||||
.viewFieldGroups,
|
||||
).map((viewFieldGroup) => viewFieldGroup.universalIdentifier);
|
||||
|
||||
const MESSAGE_CAMPAIGN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
|
||||
.universalIdentifier;
|
||||
|
||||
const HOME_TAB_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs.home
|
||||
.universalIdentifier;
|
||||
|
||||
const COMPOSER_TAB_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs
|
||||
.composer.universalIdentifier;
|
||||
|
||||
const COMPOSER_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs
|
||||
.composer.widgets.messageCampaign.universalIdentifier;
|
||||
|
||||
const HOME_FIELDS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs.home
|
||||
.widgets.fields.universalIdentifier;
|
||||
|
||||
const HOME_DETAILS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs.home
|
||||
.widgets.details.universalIdentifier;
|
||||
|
||||
const HOME_LIST_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs.home
|
||||
.widgets.list.universalIdentifier;
|
||||
|
||||
const HOME_RECIPIENTS_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs.home
|
||||
.widgets.recipients.universalIdentifier;
|
||||
|
||||
const HOME_OBSOLETE_MESSAGES_WIDGET_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs.home
|
||||
.widgets.messages.universalIdentifier;
|
||||
|
||||
@RegisteredWorkspaceCommand('2.25.0', 1785229940000)
|
||||
@Command({
|
||||
name: 'upgrade:2-25:add-message-campaign-composer-tab',
|
||||
description:
|
||||
'Aligns the message campaign record page with the Note layout: a home Fields tab (left column) plus an Email tab holding the composer editor',
|
||||
})
|
||||
export class AddMessageCampaignComposerTabCommand extends ProvisionedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
]);
|
||||
|
||||
const existingPageLayout =
|
||||
flatPageLayoutMaps.byUniversalIdentifier[
|
||||
MESSAGE_CAMPAIGN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
if (!isDefined(existingPageLayout)) {
|
||||
this.logger.log(
|
||||
`Message campaign page layout does not exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const pageLayoutTabsToCreate =
|
||||
getStandardFlatEntitiesToCreateOrThrow<FlatPageLayoutTab>({
|
||||
standardFlatEntityMaps: standardAllFlatEntityMaps.flatPageLayoutTabMaps,
|
||||
existingFlatEntityMaps: flatPageLayoutTabMaps,
|
||||
universalIdentifiers: [
|
||||
HOME_TAB_UNIVERSAL_IDENTIFIER,
|
||||
COMPOSER_TAB_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
});
|
||||
|
||||
const pageLayoutWidgetsToCreate =
|
||||
getStandardFlatEntitiesToCreateOrThrow<FlatPageLayoutWidget>({
|
||||
standardFlatEntityMaps:
|
||||
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps,
|
||||
existingFlatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
universalIdentifiers: [
|
||||
HOME_DETAILS_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
COMPOSER_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
HOME_LIST_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
HOME_RECIPIENTS_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
HOME_FIELDS_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
});
|
||||
|
||||
const viewsToCreate = getStandardFlatEntitiesToCreateOrThrow<FlatView>({
|
||||
standardFlatEntityMaps: standardAllFlatEntityMaps.flatViewMaps,
|
||||
existingFlatEntityMaps: flatViewMaps,
|
||||
universalIdentifiers: [
|
||||
MESSAGE_CAMPAIGN_RECORD_PAGE_FIELDS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
});
|
||||
|
||||
const viewFieldsToCreate =
|
||||
getStandardFlatEntitiesToCreateOrThrow<FlatViewField>({
|
||||
standardFlatEntityMaps: standardAllFlatEntityMaps.flatViewFieldMaps,
|
||||
existingFlatEntityMaps: flatViewFieldMaps,
|
||||
universalIdentifiers:
|
||||
MESSAGE_CAMPAIGN_RECORD_PAGE_FIELDS_VIEW_FIELD_UNIVERSAL_IDENTIFIERS,
|
||||
});
|
||||
|
||||
const viewFieldGroupsToCreate =
|
||||
getStandardFlatEntitiesToCreateOrThrow<FlatViewFieldGroup>({
|
||||
standardFlatEntityMaps:
|
||||
standardAllFlatEntityMaps.flatViewFieldGroupMaps,
|
||||
existingFlatEntityMaps: flatViewFieldGroupMaps,
|
||||
universalIdentifiers:
|
||||
MESSAGE_CAMPAIGN_RECORD_PAGE_FIELDS_VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIERS,
|
||||
});
|
||||
|
||||
const existingComposerTab =
|
||||
flatPageLayoutTabMaps.byUniversalIdentifier[
|
||||
COMPOSER_TAB_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
const standardComposerTab =
|
||||
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier[
|
||||
COMPOSER_TAB_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
const pageLayoutTabsToUpdate =
|
||||
isDefined(existingComposerTab) &&
|
||||
isDefined(standardComposerTab) &&
|
||||
existingComposerTab.layoutMode !== standardComposerTab.layoutMode
|
||||
? [
|
||||
{
|
||||
...existingComposerTab,
|
||||
layoutMode: standardComposerTab.layoutMode,
|
||||
title: standardComposerTab.title,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const pageLayoutWidgetsToUpdate = [
|
||||
HOME_DETAILS_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
HOME_FIELDS_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
HOME_LIST_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
HOME_RECIPIENTS_WIDGET_UNIVERSAL_IDENTIFIER,
|
||||
]
|
||||
.map((universalIdentifier) => {
|
||||
const existingWidget =
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier[universalIdentifier];
|
||||
const standardWidget =
|
||||
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps
|
||||
.byUniversalIdentifier[universalIdentifier];
|
||||
|
||||
if (
|
||||
!isDefined(existingWidget) ||
|
||||
!isDefined(standardWidget) ||
|
||||
existingWidget.conditionalAvailabilityExpression ===
|
||||
standardWidget.conditionalAvailabilityExpression
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingWidget,
|
||||
conditionalAvailabilityExpression:
|
||||
standardWidget.conditionalAvailabilityExpression,
|
||||
};
|
||||
})
|
||||
.filter((widget): widget is FlatPageLayoutWidget => isDefined(widget));
|
||||
|
||||
const obsoleteMessagesWidget =
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier[
|
||||
HOME_OBSOLETE_MESSAGES_WIDGET_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
const pageLayoutWidgetsToDelete = isDefined(obsoleteMessagesWidget)
|
||||
? [obsoleteMessagesWidget]
|
||||
: [];
|
||||
|
||||
const totalOperationCount =
|
||||
pageLayoutTabsToCreate.length +
|
||||
pageLayoutWidgetsToCreate.length +
|
||||
pageLayoutTabsToUpdate.length +
|
||||
pageLayoutWidgetsToUpdate.length +
|
||||
pageLayoutWidgetsToDelete.length +
|
||||
viewsToCreate.length +
|
||||
viewFieldsToCreate.length +
|
||||
viewFieldGroupsToCreate.length;
|
||||
|
||||
if (totalOperationCount === 0) {
|
||||
this.logger.log(
|
||||
`Message campaign record page already has the home and email tabs for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Applying ${totalOperationCount} message campaign record page operation(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: pageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: pageLayoutTabsToUpdate,
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: pageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: pageLayoutWidgetsToDelete,
|
||||
flatEntityToUpdate: pageLayoutWidgetsToUpdate,
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: viewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: viewFieldGroupsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new Error(
|
||||
`Failed to align the message campaign record page for workspace ${workspaceId}: ${JSON.stringify(
|
||||
validateAndBuildResult,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Aligned the message campaign record page for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { Command } from 'nest-commander';
|
||||
|
||||
import { STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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 { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { STANDARD_COMMAND_MENU_ITEMS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const SEND_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIERS = [
|
||||
STANDARD_COMMAND_MENU_ITEMS.sendMessageCampaign.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.sendMessageCampaignTest.universalIdentifier,
|
||||
];
|
||||
|
||||
const MESSAGE_CAMPAIGN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
|
||||
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
|
||||
.universalIdentifier;
|
||||
|
||||
const REALIGNED_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIERS = [
|
||||
STANDARD_COMMAND_MENU_ITEMS.navigateToNextRecord.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.navigateToPreviousRecord.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.addToFavorites.universalIdentifier,
|
||||
STANDARD_COMMAND_MENU_ITEMS.removeFromFavorites.universalIdentifier,
|
||||
];
|
||||
|
||||
@RegisteredWorkspaceCommand('2.25.0', 1785229960000)
|
||||
@Command({
|
||||
name: 'upgrade:2-25:configure-message-campaign-command-menu',
|
||||
description:
|
||||
'Adds the Send Campaign and Send Test Email record actions and hides the favorite/record-navigation actions on message campaign record pages in existing workspaces',
|
||||
})
|
||||
export class ConfigureMessageCampaignCommandMenuCommand extends ProvisionedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const {
|
||||
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
|
||||
flatPageLayoutMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatCommandMenuItemMaps',
|
||||
'flatPageLayoutMaps',
|
||||
]);
|
||||
|
||||
const existingPageLayout =
|
||||
flatPageLayoutMaps.byUniversalIdentifier[
|
||||
MESSAGE_CAMPAIGN_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
if (!isDefined(existingPageLayout)) {
|
||||
this.logger.log(
|
||||
`Message campaign page layout does not exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const itemsToCreate = SEND_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIERS.filter(
|
||||
(universalIdentifier) =>
|
||||
!isDefined(
|
||||
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
|
||||
universalIdentifier
|
||||
],
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(universalIdentifier) =>
|
||||
standardAllFlatEntityMaps.flatCommandMenuItemMaps
|
||||
.byUniversalIdentifier[universalIdentifier],
|
||||
)
|
||||
.filter((item): item is FlatCommandMenuItem => isDefined(item));
|
||||
|
||||
const itemsToUpdate = REALIGNED_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIERS.map(
|
||||
(universalIdentifier) => {
|
||||
const existingItem =
|
||||
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
|
||||
universalIdentifier
|
||||
];
|
||||
const standardItem =
|
||||
standardAllFlatEntityMaps.flatCommandMenuItemMaps
|
||||
.byUniversalIdentifier[universalIdentifier];
|
||||
|
||||
if (
|
||||
!isDefined(existingItem) ||
|
||||
!isDefined(standardItem) ||
|
||||
existingItem.conditionalAvailabilityExpression ===
|
||||
standardItem.conditionalAvailabilityExpression
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingItem,
|
||||
conditionalAvailabilityExpression:
|
||||
standardItem.conditionalAvailabilityExpression,
|
||||
};
|
||||
},
|
||||
).filter((item): item is FlatCommandMenuItem => isDefined(item));
|
||||
|
||||
const totalOperationCount = itemsToCreate.length + itemsToUpdate.length;
|
||||
|
||||
if (totalOperationCount === 0) {
|
||||
this.logger.log(
|
||||
`Message campaign command menu already configured for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Configuring message campaign command menu (${itemsToCreate.length} to add, ${itemsToUpdate.length} to realign) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: itemsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: itemsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new Error(
|
||||
`Failed to configure message campaign command menu for workspace ${workspaceId}: ${JSON.stringify(
|
||||
validateAndBuildResult,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Configured message campaign command menu for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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 { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const CAMPAIGN = STANDARD_OBJECTS.messageCampaign;
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
CAMPAIGN.fields.name.universalIdentifier;
|
||||
const SUBJECT_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
CAMPAIGN.fields.subject.universalIdentifier;
|
||||
const NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
CAMPAIGN.views.allMessageCampaigns.viewFields.name.universalIdentifier;
|
||||
|
||||
@RegisteredWorkspaceCommand('2.25.0', 1785229970000)
|
||||
@Command({
|
||||
name: 'upgrade:2-25:add-message-campaign-name-field',
|
||||
description:
|
||||
'Add the internal name field to messageCampaign, surface it in the all campaigns view and make it the label identifier (was subject) on existing workspaces',
|
||||
})
|
||||
export class AddMessageCampaignNameFieldCommand extends ProvisionedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatViewFieldMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatViewFieldMaps',
|
||||
]);
|
||||
|
||||
const campaignObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
universalIdentifier: CAMPAIGN.universalIdentifier,
|
||||
});
|
||||
|
||||
if (!isDefined(campaignObjectMetadata)) {
|
||||
this.logger.log(
|
||||
`messageCampaign object does not exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const fieldsToCreate: FlatFieldMetadata[] = [];
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER
|
||||
],
|
||||
)
|
||||
) {
|
||||
const standardNameField =
|
||||
findFlatEntityByUniversalIdentifier<FlatFieldMetadata>({
|
||||
flatEntityMaps: standardAllFlatEntityMaps.flatFieldMetadataMaps,
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
if (!isDefined(standardNameField)) {
|
||||
throw new Error(
|
||||
'Standard application is missing the messageCampaign name field',
|
||||
);
|
||||
}
|
||||
|
||||
fieldsToCreate.push(standardNameField);
|
||||
}
|
||||
|
||||
const viewFieldsToCreate: FlatViewField[] = [];
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
flatViewFieldMaps.byUniversalIdentifier[
|
||||
NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER
|
||||
],
|
||||
)
|
||||
) {
|
||||
const standardNameViewField =
|
||||
findFlatEntityByUniversalIdentifier<FlatViewField>({
|
||||
flatEntityMaps: standardAllFlatEntityMaps.flatViewFieldMaps,
|
||||
universalIdentifier: NAME_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
if (!isDefined(standardNameViewField)) {
|
||||
throw new Error(
|
||||
'Standard application is missing the messageCampaign name view column',
|
||||
);
|
||||
}
|
||||
|
||||
viewFieldsToCreate.push(standardNameViewField);
|
||||
}
|
||||
|
||||
// Move the label identifier from subject to name, but never clobber a
|
||||
// user customization pointing at another field.
|
||||
const currentLabelIdentifier =
|
||||
campaignObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
const flatObjectMetadataToUpdate: FlatObjectMetadata[] =
|
||||
!isDefined(currentLabelIdentifier) ||
|
||||
currentLabelIdentifier === SUBJECT_FIELD_UNIVERSAL_IDENTIFIER
|
||||
? [
|
||||
{
|
||||
...campaignObjectMetadata,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const totalOperationCount =
|
||||
fieldsToCreate.length +
|
||||
viewFieldsToCreate.length +
|
||||
flatObjectMetadataToUpdate.length;
|
||||
|
||||
if (totalOperationCount === 0) {
|
||||
this.logger.log(
|
||||
`messageCampaign name field already configured for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Workspace ${workspaceId}: ${fieldsToCreate.length} field(s), ${viewFieldsToCreate.length} view column(s), ${flatObjectMetadataToUpdate.length} label identifier update(s)`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: fieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
objectMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: flatObjectMetadataToUpdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to add the messageCampaign name field:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to add the messageCampaign name field for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Added the messageCampaign name field for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -119,6 +119,7 @@ import { WidenViewCalendarIntegrityConstraintFastInstanceCommand } from './2-23/
|
||||
import { AddApplicationIdToKeyValuePairFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784659343818-add-application-id-to-key-value-pair';
|
||||
import { AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand } from './2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration';
|
||||
import { AddMessageChannelDisplayNameFastInstanceCommand } from './2-25/2-25-instance-command-fast-1784904030252-add-message-channel-display-name';
|
||||
import { AddMessageCampaignWidgetTypeFastInstanceCommand } from './2-25/2-25-instance-command-fast-1785229950000-add-message-campaign-widget-type';
|
||||
import { BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784286705000-backfill-created-workspace-activation-status';
|
||||
import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784322591746-unlist-unclaimed-npm-application-registrations';
|
||||
import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784650048045-add-statuses-to-billing-subscription-index';
|
||||
@@ -251,6 +252,7 @@ export const INSTANCE_COMMANDS = [
|
||||
AddSdkClientCoreChecksumToApplicationFastInstanceCommand,
|
||||
AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand,
|
||||
AddMessageChannelDisplayNameFastInstanceCommand,
|
||||
AddMessageCampaignWidgetTypeFastInstanceCommand,
|
||||
AddStatusesToBillingSubscriptionIndexSlowInstanceCommand,
|
||||
AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand,
|
||||
RepairKeyValuePairApplicationIdFastInstanceCommand,
|
||||
|
||||
Reference in New Issue
Block a user