[WIP] Feat/marketing emails (#21173)

Marketing/campaign emails on top of the emailing-domain (SES) feature:
send a broadcast to a hand-picked list, with per-customer-domain
unsubscribe links and opt-out-only **unsubscribe topics**.

## Model

Standard objects (workspace schema, flat-metadata):
- `messageCampaign` — a campaign send (subject, body template, from
address, status, list, optional unsubscribe topic).
- `messageList` + `messageListMember` — the hand-picked audience (person
↔ list join). A campaign's recipients are its list's members; everyone
is sendable unless suppressed.

Core entities (`core` schema, workspace-scoped — readable by the public
unsubscribe flow without a workspace context):
- `unsubscribeTopic` — an opt-out-only category (name, description,
visibility). There is no opt-in subscription state.
- `messageSuppression` — the single consent store: a row with
`unsubscribeTopicId` NULL is a global block; a row with an
`unsubscribeTopicId` and reason `UNSUBSCRIBE` is a per-topic opt-out.
Two partial unique indexes dedupe global vs per-topic rows (Postgres
treats NULLs as distinct).
- `emailingDomain` — the workspace's SES sending domain,
auto-provisioned when an email channel is added (and cleaned up when its
last channel is removed), with verification status + DNS records.

Campaign messages reuse the existing `message` / `messageThread` /
`messageParticipant` model — one outbound `message` per recipient with a
`deliveryStatus` state machine.

## Sending

- `sendMessageCampaign` resolves the audience **under the caller's
permissions**, creates the campaign, and enqueues a single fan-out job
(the request never materializes per-recipient rows or jobs).
- The fan-out job materializes one QUEUED message per recipient
(deterministic ids → idempotent re-runs, reconciles crash-orphaned rows)
and fans out per-recipient send jobs carrying **only ids**.
- Each send job renders per-recipient `{{variable}}` merge fields and
sends via `EmailingDomainSenderService`, which applies suppression
(global + per-topic) and the unsubscribe footer/headers. Suppressed
recipients are recorded `SKIPPED`.
- The campaign finalizes `SENT`, or `SENT_WITH_ERRORS` if any recipient
terminally failed.
- `previewMessageCampaignAudience` returns a pre-send breakdown (total /
without-email / duplicate / globally-unsubscribed / topic-unsubscribed /
sendable), shown as a hint under the composer pickers.

## Unsubscribe

- Encrypted (AES-256-GCM) token carrying workspaceId, address, optional
`unsubscribeTopicId`, `issuedAt`, and a `preview` flag.
- One-click POST (RFC 8058) + `mailto:` — topic-scoped when the token
carries a topic, global otherwise.
- Preferences page: a checkbox per visible topic (checked = still
receiving); submitting creates per-topic opt-outs for unchecked topics
and lifts re-checked ones (UNSUBSCRIBE only — never
`BOUNCE`/`COMPLAINT`, never a global block).
- A **Preview** action in settings opens the live page via a
preview-claim token; opt-out POSTs are no-ops for preview tokens, so
previewing never mutates state.
- SES webhooks: inbound unsubscribe + outbound bounce/complaint →
suppression (race-safe against at-least-once delivery, with reason
escalation that never downgrades).
- Per-customer unsubscribe hostname (Cloudflare DNS); sends are gated on
it being active, except in LOG/demo mode.

## Architecture

Campaign orchestration, suppression, the sender, the unsubscribe
controller, and the SES webhook handlers live in `src/modules/emailing`
+ `src/modules/messaging-webhooks` (the workspace-feature layer).
`core-modules/emailing-domain` keeps the SES driver, domain
provisioning, the `unsubscribeTopic` / `messageSuppression` core
entities, and the unsubscribe token/hostname plumbing. Domain creation
is validated (`CreateEmailingDomainInput` — domain-format regex,
lowercased) before any value reaches SES or the unsubscribe hostname.

## Frontend

- Campaign composer side panel (from / list / unsubscribe topic /
subject / body) with a live audience-preview hint.
- Email settings: email channels each showing their auto-provisioned
sending domain in a single section (status + DNS records + a "Check
verification" action), plus an **Unsubscribe Topics** section to
create/manage topics and preview the recipient page. A demo-mode banner
is shown when the LOG driver is active.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2026-06-13 22:07:39 +05:30
committed by GitHub
parent 2c5da39dc5
commit 5d892bdfd0
238 changed files with 10433 additions and 3517 deletions
@@ -5,4 +5,6 @@ export const MESSAGE_CHANNEL_DATA_SEED_IDS = {
JANE: '20202020-8c4d-4e71-a672-2e6a8c9f1b3d',
SUPPORT: '20202020-e2f1-49b5-85d2-5d3a3386990d',
SALES: '20202020-e2f1-49b5-85d2-5d3a3386990e',
SUPPORT_GROUP: '20202020-5a1e-4b2c-9d3e-200000000001',
CONTACT_GROUP: '20202020-5a1e-4b2c-9d3e-200000000002',
} as const;
@@ -7,6 +7,15 @@ const tableName = 'emailingDomain';
const DEV_EMAILING_DOMAIN = 'dev.twenty.local';
export const getSeededEmailGroupDomains = (workspaceId: string) => {
const prefix = workspaceId.slice(0, 8);
return {
verified: `${prefix}.${DEV_EMAILING_DOMAIN}`,
pending: `${prefix}.pending.${DEV_EMAILING_DOMAIN}`,
};
};
type SeedEmailingDomainsArgs = {
queryRunner: QueryRunner;
schemaName: string;
@@ -18,7 +27,7 @@ export const seedEmailingDomains = async ({
schemaName,
workspaceId,
}: SeedEmailingDomainsArgs) => {
const domain = `${workspaceId.slice(0, 8)}.${DEV_EMAILING_DOMAIN}`;
const { verified, pending } = getSeededEmailGroupDomains(workspaceId);
await queryRunner.manager
.createQueryBuilder()
@@ -35,12 +44,31 @@ export const seedEmailingDomains = async ({
.values([
{
workspaceId,
domain,
domain: verified,
status: EmailingDomainStatus.VERIFIED,
verificationRecords: JSON.stringify([]),
verificationRecords: [],
verifiedAt: new Date(),
tenantStatus: EmailingDomainTenantStatus.ACTIVE,
},
{
workspaceId,
domain: pending,
status: EmailingDomainStatus.PENDING,
verificationRecords: [
{
type: 'TXT',
key: `_amazonses.${pending}`,
value: 'seed-verification-token',
},
{
type: 'CNAME',
key: `seed1._domainkey.${pending}`,
value: 'seed1.dkim.amazonses.com',
},
],
verifiedAt: null,
tenantStatus: EmailingDomainTenantStatus.ACTIVE,
},
])
.execute();
};
@@ -16,6 +16,8 @@ import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-s
import { CALENDAR_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/calendar-channel-seed-ids.constant';
import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/message-channel-seed-ids.constant';
import { MESSAGE_FOLDER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/constants/message-folder-seed-ids.constant';
import { UnsubscribeTopicVisibility } from 'src/engine/core-modules/emailing-domain/types/unsubscribe-topic-visibility.type';
import { getSeededEmailGroupDomains } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-emailing-domains.util';
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
type SeedMetadataEntitiesArgs = {
@@ -31,6 +33,8 @@ const YC_CONNECTED_ACCOUNT_IDS = {
PHIL: '30303030-cafc-4323-908d-e5b42ad69fdf',
JANE: '30303030-b5c7-46f0-bf5c-3f4e4b3f7c1a',
JANE_DELETABLE: '30303030-d1e5-4a8f-9c3b-7f6d5e4c3b2a',
SUPPORT_GROUP: '30303030-5a1e-4b2c-9d3e-100000000001',
CONTACT_GROUP: '30303030-5a1e-4b2c-9d3e-100000000002',
};
const YC_MESSAGE_CHANNEL_IDS = {
@@ -40,6 +44,8 @@ const YC_MESSAGE_CHANNEL_IDS = {
JANE: '30303030-8c4d-4e71-a672-2e6a8c9f1b3d',
SUPPORT: '30303030-e2f1-49b5-85d2-5d3a3386990d',
SALES: '30303030-e2f1-49b5-85d2-5d3a3386990e',
SUPPORT_GROUP: '30303030-5a1e-4b2c-9d3e-200000000001',
CONTACT_GROUP: '30303030-5a1e-4b2c-9d3e-200000000002',
} as const;
const YC_CALENDAR_CHANNEL_IDS = {
@@ -59,6 +65,18 @@ const YC_MESSAGE_FOLDER_IDS = {
JANE_SENT: '30303030-1234-4567-8901-abcdef012348',
} as const;
const APPLE_UNSUBSCRIBE_TOPIC_IDS = {
PRODUCT_UPDATES: '20202020-7b1c-4a2d-8e3f-300000000001',
NEWSLETTER: '20202020-7b1c-4a2d-8e3f-300000000002',
TRANSACTIONAL: '20202020-7b1c-4a2d-8e3f-300000000003',
} as const;
const YC_UNSUBSCRIBE_TOPIC_IDS = {
PRODUCT_UPDATES: '30303030-7b1c-4a2d-8e3f-300000000001',
NEWSLETTER: '30303030-7b1c-4a2d-8e3f-300000000002',
TRANSACTIONAL: '30303030-7b1c-4a2d-8e3f-300000000003',
} as const;
const getSeedIds = (workspaceId: string) => {
if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
return {
@@ -72,6 +90,7 @@ const getSeedIds = (workspaceId: string) => {
messageChannelIds: YC_MESSAGE_CHANNEL_IDS,
calendarChannelIds: YC_CALENDAR_CHANNEL_IDS,
messageFolderIds: YC_MESSAGE_FOLDER_IDS,
unsubscribeTopicIds: YC_UNSUBSCRIBE_TOPIC_IDS,
};
}
@@ -86,6 +105,7 @@ const getSeedIds = (workspaceId: string) => {
messageChannelIds: MESSAGE_CHANNEL_DATA_SEED_IDS,
calendarChannelIds: CALENDAR_CHANNEL_DATA_SEED_IDS,
messageFolderIds: MESSAGE_FOLDER_DATA_SEED_IDS,
unsubscribeTopicIds: APPLE_UNSUBSCRIBE_TOPIC_IDS,
};
};
@@ -103,6 +123,7 @@ export const seedMetadataEntities = async ({
await seedConnectedAccounts({ queryRunner, schemaName, workspaceId });
await seedMessageChannels({ queryRunner, schemaName, workspaceId });
await seedUnsubscribeTopics({ queryRunner, schemaName, workspaceId });
await seedCalendarChannels({ queryRunner, schemaName, workspaceId });
await seedMessageFolders({ queryRunner, schemaName, workspaceId });
};
@@ -113,6 +134,7 @@ const seedConnectedAccounts = async ({
workspaceId,
}: SeedMetadataEntitiesArgs) => {
const ids = getSeedIds(workspaceId);
const emailGroupDomains = getSeededEmailGroupDomains(workspaceId);
const connectedAccounts = [
{
@@ -150,6 +172,20 @@ const seedConnectedAccounts = async ({
userWorkspaceId: ids.userWorkspaceIds.JANE,
workspaceId,
},
{
id: ids.connectedAccountIds.SUPPORT_GROUP,
handle: `support@${emailGroupDomains.verified}`,
provider: 'email_group',
userWorkspaceId: ids.userWorkspaceIds.TIM,
workspaceId,
},
{
id: ids.connectedAccountIds.CONTACT_GROUP,
handle: `contact@${emailGroupDomains.pending}`,
provider: 'email_group',
userWorkspaceId: ids.userWorkspaceIds.TIM,
workspaceId,
},
];
await queryRunner.manager
@@ -271,6 +307,38 @@ const seedMessageChannels = async ({
connectedAccountId: ids.connectedAccountIds.TIM,
workspaceId,
},
{
id: ids.messageChannelIds.SUPPORT_GROUP,
handle: 'emailgroup-support@demo.invalid',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
type: MessageChannelType.EMAIL_GROUP,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
isContactAutoCreationEnabled: true,
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
messageFolderImportPolicy: 'ALL_FOLDERS',
excludeNonProfessionalEmails: false,
excludeGroupEmails: false,
pendingGroupEmailsAction: 'NONE',
isSyncEnabled: true,
connectedAccountId: ids.connectedAccountIds.SUPPORT_GROUP,
workspaceId,
},
{
id: ids.messageChannelIds.CONTACT_GROUP,
handle: 'emailgroup-contact@demo.invalid',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
type: MessageChannelType.EMAIL_GROUP,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
isContactAutoCreationEnabled: true,
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
messageFolderImportPolicy: 'ALL_FOLDERS',
excludeNonProfessionalEmails: false,
excludeGroupEmails: false,
pendingGroupEmailsAction: 'NONE',
isSyncEnabled: true,
connectedAccountId: ids.connectedAccountIds.CONTACT_GROUP,
workspaceId,
},
];
await queryRunner.manager
@@ -297,6 +365,52 @@ const seedMessageChannels = async ({
.execute();
};
const seedUnsubscribeTopics = async ({
queryRunner,
schemaName,
workspaceId,
}: SeedMetadataEntitiesArgs) => {
const ids = getSeedIds(workspaceId);
const unsubscribeTopics = [
{
id: ids.unsubscribeTopicIds.PRODUCT_UPDATES,
name: 'Product updates',
description: 'New features and product announcements.',
visibility: UnsubscribeTopicVisibility.PUBLIC,
workspaceId,
},
{
id: ids.unsubscribeTopicIds.NEWSLETTER,
name: 'Newsletter',
description: 'Our periodic company newsletter.',
visibility: UnsubscribeTopicVisibility.PUBLIC,
workspaceId,
},
{
id: ids.unsubscribeTopicIds.TRANSACTIONAL,
name: 'Transactional',
description: 'Internal-only category, hidden from the preferences page.',
visibility: UnsubscribeTopicVisibility.PRIVATE,
workspaceId,
},
];
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(`${schemaName}.unsubscribeTopic`, [
'id',
'name',
'description',
'visibility',
'workspaceId',
])
.orIgnore()
.values(unsubscribeTopics)
.execute();
};
const seedCalendarChannels = async ({
queryRunner,
schemaName,
@@ -33,6 +33,8 @@ export const CONNECTED_ACCOUNT_DATA_SEED_IDS = {
PHIL: '20202020-cafc-4323-908d-e5b42ad69fdf',
JANE: '20202020-b5c7-46f0-bf5c-3f4e4b3f7c1a',
JANE_DELETABLE: '20202020-d1e5-4a8f-9c3b-7f6d5e4c3b2a',
SUPPORT_GROUP: '20202020-5a1e-4b2c-9d3e-100000000001',
CONTACT_GROUP: '20202020-5a1e-4b2c-9d3e-100000000002',
};
export const CONNECTED_ACCOUNT_DATA_SEEDS: ConnectedAccountDataSeed[] = [
@@ -709,6 +709,36 @@ export const STANDARD_COMMAND_MENU_ITEMS = {
engineComponentKey: EngineComponentKey.COMPOSE_EMAIL,
hotKeys: null,
},
composeCampaign: {
universalIdentifier: '30473656-e7cb-42e0-b198-6c4e8b906106',
label: 'Compose Campaign',
icon: 'IconSend',
isPinned: false,
position: 66,
shortLabel: 'Campaign',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: 'featureFlags.IS_EMAIL_GROUP_ENABLED',
availabilityObjectMetadataUniversalIdentifier: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.COMPOSE_CAMPAIGN,
hotKeys: null,
},
composeCampaignPinned: {
universalIdentifier: '7ad6f0c7-ac02-4062-b5cf-1f36e1664bc8',
label: 'Compose Campaign',
icon: 'IconSend',
isPinned: true,
position: 67,
shortLabel: 'Campaign',
availabilityType: CommandMenuItemAvailabilityType.GLOBAL_OBJECT_CONTEXT,
conditionalAvailabilityExpression:
'pageType == "INDEX_PAGE" and featureFlags.IS_EMAIL_GROUP_ENABLED',
availabilityObjectMetadataUniversalIdentifier:
STANDARD_OBJECTS.messageCampaign.universalIdentifier,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.COMPOSE_CAMPAIGN,
hotKeys: null,
},
goToSettings: {
universalIdentifier: 'ef9aba44-0068-453e-930a-f8c182af18ee',
label: 'Go to Settings',
@@ -85,6 +85,10 @@ export const VERTICAL_LIST_LAYOUT_POSITIONS = {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 3,
},
FIFTH: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 4,
},
} as const satisfies Record<string, PageLayoutWidgetVerticalListPosition>;
export const CANVAS_LAYOUT_POSITIONS = {
@@ -5,9 +5,11 @@ import {
STANDARD_CALL_RECORDING_PAGE_LAYOUT_CONFIG,
STANDARD_COMPANY_PAGE_LAYOUT_CONFIG,
STANDARD_DASHBOARD_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_CAMPAIGN_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_MESSAGE_FOLDER_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_PARTICIPANT_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_LIST_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_THREAD_PAGE_LAYOUT_CONFIG,
STANDARD_NOTE_PAGE_LAYOUT_CONFIG,
STANDARD_OPPORTUNITY_PAGE_LAYOUT_CONFIG,
@@ -29,11 +31,13 @@ export const STANDARD_PAGE_LAYOUTS = {
STANDARD_CALENDAR_EVENT_PARTICIPANT_PAGE_LAYOUT_CONFIG,
callRecordingRecordPage: STANDARD_CALL_RECORDING_PAGE_LAYOUT_CONFIG,
companyRecordPage: STANDARD_COMPANY_PAGE_LAYOUT_CONFIG,
messageCampaignRecordPage: STANDARD_MESSAGE_CAMPAIGN_PAGE_LAYOUT_CONFIG,
messageChannelMessageAssociationRecordPage:
STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_PAGE_LAYOUT_CONFIG,
messageChannelMessageAssociationMessageFolderRecordPage:
STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_MESSAGE_FOLDER_PAGE_LAYOUT_CONFIG,
messageParticipantRecordPage: STANDARD_MESSAGE_PARTICIPANT_PAGE_LAYOUT_CONFIG,
messageListRecordPage: STANDARD_MESSAGE_LIST_PAGE_LAYOUT_CONFIG,
messageThreadRecordPage: STANDARD_MESSAGE_THREAD_PAGE_LAYOUT_CONFIG,
noteRecordPage: STANDARD_NOTE_PAGE_LAYOUT_CONFIG,
opportunityRecordPage: STANDARD_OPPORTUNITY_PAGE_LAYOUT_CONFIG,
@@ -153,28 +153,7 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
},
"messageChannelMessageAssociationMessageFolderRecordPage": {
"id": "00000000-0000-0000-0000-000000000053",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000054",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000055",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000056",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000057",
},
},
},
},
},
"messageChannelMessageAssociationRecordPage": {
"messageCampaignRecordPage": {
"id": "00000000-0000-0000-0000-000000000048",
"tabs": {
"home": {
@@ -183,47 +162,106 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
"fields": {
"id": "00000000-0000-0000-0000-000000000050",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000051",
"widgets": {
"timeline": {
"list": {
"id": "00000000-0000-0000-0000-000000000051",
},
"messages": {
"id": "00000000-0000-0000-0000-000000000053",
},
"recipients": {
"id": "00000000-0000-0000-0000-000000000052",
},
},
},
},
},
"messageParticipantRecordPage": {
"id": "00000000-0000-0000-0000-000000000058",
"messageChannelMessageAssociationMessageFolderRecordPage": {
"id": "00000000-0000-0000-0000-000000000059",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000059",
"id": "00000000-0000-0000-0000-000000000060",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000060",
"id": "00000000-0000-0000-0000-000000000061",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000061",
"id": "00000000-0000-0000-0000-000000000062",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000062",
"id": "00000000-0000-0000-0000-000000000063",
},
},
},
},
},
"messageChannelMessageAssociationRecordPage": {
"id": "00000000-0000-0000-0000-000000000054",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000055",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000056",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000057",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000058",
},
},
},
},
},
"messageListRecordPage": {
"id": "00000000-0000-0000-0000-000000000069",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000070",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000071",
},
"members": {
"id": "00000000-0000-0000-0000-000000000072",
},
},
},
},
},
"messageParticipantRecordPage": {
"id": "00000000-0000-0000-0000-000000000064",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000065",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000066",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000067",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000068",
},
},
},
},
},
"messageThreadRecordPage": {
"id": "00000000-0000-0000-0000-000000000063",
"id": "00000000-0000-0000-0000-000000000073",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000064",
"id": "00000000-0000-0000-0000-000000000074",
"widgets": {
"emailThread": {
"id": "00000000-0000-0000-0000-000000000065",
"id": "00000000-0000-0000-0000-000000000075",
},
},
},
@@ -264,102 +302,32 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
"noteRecordPage": {
"id": "00000000-0000-0000-0000-000000000066",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000074",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000075",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000067",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000068",
},
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000069",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000070",
"widgets": {
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000071",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000072",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000073",
},
},
},
},
},
"opportunityRecordPage": {
"id": "00000000-0000-0000-0000-000000000076",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000092",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000093",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000090",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000091",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000088",
"id": "00000000-0000-0000-0000-000000000084",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000089",
"id": "00000000-0000-0000-0000-000000000085",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000077",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000080",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000078",
},
"owner": {
"id": "00000000-0000-0000-0000-000000000081",
},
"pointOfContact": {
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000079",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000086",
"note": {
"id": "00000000-0000-0000-0000-000000000080",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000087",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000084",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000085",
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000081",
},
},
},
@@ -373,190 +341,263 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
},
"personRecordPage": {
"id": "00000000-0000-0000-0000-000000000094",
"opportunityRecordPage": {
"id": "00000000-0000-0000-0000-000000000086",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000109",
"id": "00000000-0000-0000-0000-000000000102",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000110",
"id": "00000000-0000-0000-0000-000000000103",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000107",
"id": "00000000-0000-0000-0000-000000000100",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000108",
"id": "00000000-0000-0000-0000-000000000101",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000105",
"id": "00000000-0000-0000-0000-000000000098",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000106",
"id": "00000000-0000-0000-0000-000000000099",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000095",
"id": "00000000-0000-0000-0000-000000000087",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000097",
"id": "00000000-0000-0000-0000-000000000090",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000096",
"id": "00000000-0000-0000-0000-000000000088",
},
"pointOfContactForOpportunities": {
"id": "00000000-0000-0000-0000-000000000098",
"owner": {
"id": "00000000-0000-0000-0000-000000000091",
},
"pointOfContact": {
"id": "00000000-0000-0000-0000-000000000089",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000103",
"id": "00000000-0000-0000-0000-000000000096",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000104",
"id": "00000000-0000-0000-0000-000000000097",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000101",
"id": "00000000-0000-0000-0000-000000000094",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000102",
"id": "00000000-0000-0000-0000-000000000095",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000099",
"id": "00000000-0000-0000-0000-000000000092",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000100",
"id": "00000000-0000-0000-0000-000000000093",
},
},
},
},
},
"personRecordPage": {
"id": "00000000-0000-0000-0000-000000000104",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000120",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000121",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000118",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000119",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000116",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000117",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000105",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000107",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000106",
},
"listMemberships": {
"id": "00000000-0000-0000-0000-000000000109",
},
"pointOfContactForOpportunities": {
"id": "00000000-0000-0000-0000-000000000108",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000114",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000115",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000112",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000113",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000110",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000111",
},
},
},
},
},
"taskRecordPage": {
"id": "00000000-0000-0000-0000-000000000111",
"id": "00000000-0000-0000-0000-000000000122",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000119",
"id": "00000000-0000-0000-0000-000000000130",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000120",
"id": "00000000-0000-0000-0000-000000000131",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000112",
"id": "00000000-0000-0000-0000-000000000123",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000113",
"id": "00000000-0000-0000-0000-000000000124",
},
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000114",
"id": "00000000-0000-0000-0000-000000000125",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000115",
"id": "00000000-0000-0000-0000-000000000126",
"widgets": {
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000116",
"id": "00000000-0000-0000-0000-000000000127",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000117",
"id": "00000000-0000-0000-0000-000000000128",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000118",
"id": "00000000-0000-0000-0000-000000000129",
},
},
},
},
},
"workflowAutomatedTriggerRecordPage": {
"id": "00000000-0000-0000-0000-000000000124",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000125",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000126",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000127",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000128",
},
},
},
},
},
"workflowRecordPage": {
"id": "00000000-0000-0000-0000-000000000121",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000122",
"widgets": {
"workflow": {
"id": "00000000-0000-0000-0000-000000000123",
},
},
},
},
},
"workflowRunRecordPage": {
"id": "00000000-0000-0000-0000-000000000135",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000139",
"widgets": {
"workflowRun": {
"id": "00000000-0000-0000-0000-000000000140",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000136",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000137",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000138",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000139",
},
},
},
},
},
"workflowRecordPage": {
"id": "00000000-0000-0000-0000-000000000132",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000133",
"widgets": {
"workflow": {
"id": "00000000-0000-0000-0000-000000000138",
"id": "00000000-0000-0000-0000-000000000134",
},
},
},
},
},
"workflowRunRecordPage": {
"id": "00000000-0000-0000-0000-000000000146",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000150",
"widgets": {
"workflowRun": {
"id": "00000000-0000-0000-0000-000000000151",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000147",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000148",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000149",
},
},
},
},
},
"workflowVersionRecordPage": {
"id": "00000000-0000-0000-0000-000000000129",
"id": "00000000-0000-0000-0000-000000000140",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000133",
"id": "00000000-0000-0000-0000-000000000144",
"widgets": {
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000134",
"id": "00000000-0000-0000-0000-000000000145",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000130",
"id": "00000000-0000-0000-0000-000000000141",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000131",
"id": "00000000-0000-0000-0000-000000000142",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000132",
"id": "00000000-0000-0000-0000-000000000143",
},
},
},
@@ -13,6 +13,9 @@ import { buildCalendarEventStandardFlatFieldMetadatas } from 'src/engine/workspa
import { buildCallRecordingStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-call-recording-standard-flat-field-metadata.util';
import { buildCompanyStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-company-standard-flat-field-metadata.util';
import { buildDashboardStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-dashboard-standard-flat-field-metadata.util';
import { buildMessageCampaignStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-campaign-standard-flat-field-metadata.util';
import { buildMessageListStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-list-standard-flat-field-metadata.util';
import { buildMessageListMemberStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-list-member-standard-flat-field-metadata.util';
import { buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-message-folder-standard-flat-field-metadata.util';
import { buildMessageChannelMessageAssociationStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-standard-flat-field-metadata.util';
import { buildMessageParticipantStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-participant-standard-flat-field-metadata.util';
@@ -47,6 +50,9 @@ const STANDARD_FLAT_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME = {
callRecording: buildCallRecordingStandardFlatFieldMetadatas,
company: buildCompanyStandardFlatFieldMetadatas,
dashboard: buildDashboardStandardFlatFieldMetadatas,
messageCampaign: buildMessageCampaignStandardFlatFieldMetadatas,
messageList: buildMessageListStandardFlatFieldMetadatas,
messageListMember: buildMessageListMemberStandardFlatFieldMetadatas,
message: buildMessageStandardFlatFieldMetadatas,
messageChannelMessageAssociation:
buildMessageChannelMessageAssociationStandardFlatFieldMetadatas,
@@ -0,0 +1,361 @@
import { msg } from '@lingui/core/macro';
import { i18nLabel } from 'src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util';
import {
DateDisplayFormat,
FieldMetadataType,
RelationOnDeleteAction,
RelationType,
} from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
import {
type CreateStandardFieldArgs,
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_FOR_MESSAGE_CAMPAIGN } from 'src/modules/emailing/standard-objects/message-campaign.workspace-entity';
export const buildMessageCampaignStandardFlatFieldMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<
CreateStandardFieldArgs<'messageCampaign', FieldMetadataType>,
'context'
>): Record<
AllStandardObjectFieldName<'messageCampaign'>,
FlatFieldMetadata
> => {
const base = {
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
objectName,
workspaceId,
};
return {
id: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'id',
type: FieldMetadataType.UUID,
label: i18nLabel(msg`Id`),
description: i18nLabel(msg`Id`),
icon: 'Icon123',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'uuid',
},
}),
createdAt: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'createdAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Creation date`),
description: i18nLabel(msg`Creation date`),
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'now',
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
}),
updatedAt: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'updatedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Last update`),
description: i18nLabel(msg`Last time the record was changed`),
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'now',
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
}),
deletedAt: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'deletedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Deleted at`),
description: i18nLabel(msg`Date when the record was deleted`),
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIEditable: false,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
}),
createdBy: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'createdBy',
type: FieldMetadataType.ACTOR,
label: i18nLabel(msg`Created by`),
description: i18nLabel(msg`The creator of the record`),
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIEditable: false,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
}),
updatedBy: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: i18nLabel(msg`Updated by`),
description: i18nLabel(
msg`The workspace member who last updated the record`,
),
icon: 'IconUserCircle',
isSystem: true,
isUIEditable: false,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
}),
position: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'position',
type: FieldMetadataType.POSITION,
label: i18nLabel(msg`Position`),
description: i18nLabel(msg`Email campaign record position`),
icon: 'IconHierarchy2',
isSystem: true,
isNullable: false,
defaultValue: 0,
},
}),
searchVector: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'searchVector',
type: FieldMetadataType.TS_VECTOR,
label: i18nLabel(msg`Search vector`),
description: i18nLabel(msg`Field used for full-text search`),
icon: 'IconSend',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_FOR_MESSAGE_CAMPAIGN,
),
},
},
}),
subject: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'subject',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Subject`),
description: i18nLabel(msg`Email subject line`),
icon: 'IconMail',
isNullable: true,
isUIEditable: false,
},
}),
bodyTemplate: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'bodyTemplate',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Body`),
description: i18nLabel(msg`Email body sent to recipients`),
icon: 'IconFileText',
isNullable: true,
isUIEditable: false,
},
}),
fromAddress: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'fromAddress',
type: FieldMetadataType.EMAILS,
label: i18nLabel(msg`From address`),
description: i18nLabel(msg`Sender address for the campaign`),
icon: 'IconAt',
isNullable: true,
isUIEditable: false,
settings: {
maxNumberOfValues: 1,
},
},
}),
status: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'status',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Status`),
description: i18nLabel(msg`Campaign lifecycle status`),
icon: 'IconProgress',
isNullable: false,
isUIEditable: false,
defaultValue: "'DRAFT'",
options: [
{
id: '2bebe786-69e0-4673-8781-a85588b77c44',
value: 'DRAFT',
label: i18nLabel(msg`Draft`),
position: 0,
color: 'gray',
},
{
id: 'dba0c513-d1dc-4c6a-980a-40795bdb0759',
value: 'SCHEDULED',
label: i18nLabel(msg`Scheduled`),
position: 1,
color: 'blue',
},
{
id: '575b9ed5-1123-480c-9821-c73410841347',
value: 'SENDING',
label: i18nLabel(msg`Sending`),
position: 2,
color: 'yellow',
},
{
id: '0c311eae-0892-4319-84e6-b30e921dc01a',
value: 'SENT',
label: i18nLabel(msg`Sent`),
position: 3,
color: 'green',
},
{
id: 'c309536c-ceb7-4510-8481-c2cbd88ffe96',
value: 'SENT_WITH_ERRORS',
label: i18nLabel(msg`Sent with errors`),
position: 4,
color: 'orange',
},
],
},
}),
sentAt: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'sentAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Sent at`),
description: i18nLabel(msg`When the campaign finished sending`),
icon: 'IconSend',
isNullable: true,
isUIEditable: false,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
}),
unsubscribeTopicId: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'unsubscribeTopicId',
type: FieldMetadataType.UUID,
label: i18nLabel(msg`Unsubscribe topic id`),
description: i18nLabel(
msg`The unsubscribe topic this campaign was sent under`,
),
icon: 'IconMailbox',
isNullable: true,
isUIEditable: false,
},
}),
list: createStandardRelationFieldFlatMetadata({
...base,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'list',
label: i18nLabel(msg`List`),
description: i18nLabel(msg`The list this campaign was sent to`),
icon: 'IconUsersGroup',
isNullable: true,
isUIEditable: false,
targetObjectName: 'messageList',
targetFieldName: 'campaigns',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'listId',
},
},
}),
timelineActivities: createStandardRelationFieldFlatMetadata({
...base,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'timelineActivities',
label: i18nLabel(msg`Events`),
description: i18nLabel(msg`Events linked to the campaign`),
icon: 'IconTimelineEvent',
isNullable: true,
targetObjectName: 'timelineActivity',
targetFieldName: 'targetMessageCampaign',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
}),
messages: createStandardRelationFieldFlatMetadata({
...base,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'messages',
label: i18nLabel(msg`Messages`),
description: i18nLabel(msg`Messages sent as part of this campaign`),
icon: 'IconMessage',
isNullable: true,
isUIEditable: false,
targetObjectName: 'message',
targetFieldName: 'messageCampaign',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
}),
recipients: createStandardRelationFieldFlatMetadata({
...base,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'recipients',
label: i18nLabel(msg`Recipients`),
description: i18nLabel(msg`The people this campaign was sent to`),
icon: 'IconUsers',
isNullable: true,
isUIEditable: false,
targetObjectName: 'messageParticipant',
targetFieldName: 'messageCampaign',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
}),
};
};
@@ -0,0 +1,248 @@
import { msg } from '@lingui/core/macro';
import { i18nLabel } from 'src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util';
import {
DateDisplayFormat,
FieldMetadataType,
RelationOnDeleteAction,
RelationType,
} from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
import {
type CreateStandardFieldArgs,
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
export const buildMessageListMemberStandardFlatFieldMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<
CreateStandardFieldArgs<'messageListMember', FieldMetadataType>,
'context'
>): Record<
AllStandardObjectFieldName<'messageListMember'>,
FlatFieldMetadata
> => ({
id: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'id',
type: FieldMetadataType.UUID,
label: i18nLabel(msg`Id`),
description: i18nLabel(msg`Id`),
icon: 'Icon123',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'uuid',
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
createdAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'createdAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Creation date`),
description: i18nLabel(msg`Creation date`),
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'now',
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
updatedAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Last update`),
description: i18nLabel(msg`Last time the record was changed`),
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'now',
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
deletedAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'deletedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Deleted at`),
description: i18nLabel(msg`Date when the record was deleted`),
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIEditable: false,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
createdBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'createdBy',
type: FieldMetadataType.ACTOR,
label: i18nLabel(msg`Created by`),
description: i18nLabel(msg`The creator of the record`),
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIEditable: false,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: i18nLabel(msg`Updated by`),
description: i18nLabel(
msg`The workspace member who last updated the record`,
),
icon: 'IconUserCircle',
isSystem: true,
isUIEditable: false,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
position: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'position',
type: FieldMetadataType.POSITION,
label: i18nLabel(msg`Position`),
description: i18nLabel(msg`List member record position`),
icon: 'IconHierarchy2',
isSystem: true,
isNullable: false,
defaultValue: 0,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
list: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'list',
label: i18nLabel(msg`List`),
description: i18nLabel(msg`The list the person belongs to`),
icon: 'IconUsersGroup',
isNullable: false,
targetObjectName: 'messageList',
targetFieldName: 'members',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.CASCADE,
joinColumnName: 'listId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
person: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'person',
label: i18nLabel(msg`Person`),
description: i18nLabel(msg`The person in the list`),
icon: 'IconUser',
isNullable: false,
targetObjectName: 'person',
targetFieldName: 'listMemberships',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.CASCADE,
joinColumnName: 'personId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'searchVector',
type: FieldMetadataType.TS_VECTOR,
label: i18nLabel(msg`Search vector`),
description: i18nLabel(msg`Field used for full-text search`),
icon: 'IconUser',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([
{ name: 'id', type: FieldMetadataType.UUID },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -0,0 +1,284 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { i18nLabel } from 'src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util';
import {
DateDisplayFormat,
FieldMetadataType,
RelationType,
} from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
import {
type CreateStandardFieldArgs,
createStandardFieldFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { SEARCH_FIELDS_FOR_MESSAGE_LIST } from 'src/modules/emailing/standard-objects/message-list.workspace-entity';
export const buildMessageListStandardFlatFieldMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<
CreateStandardFieldArgs<'messageList', FieldMetadataType>,
'context'
>): Record<AllStandardObjectFieldName<'messageList'>, FlatFieldMetadata> => ({
id: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'id',
type: FieldMetadataType.UUID,
label: i18nLabel(msg`Id`),
description: i18nLabel(msg`Id`),
icon: 'Icon123',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'uuid',
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
createdAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'createdAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Creation date`),
description: i18nLabel(msg`Creation date`),
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'now',
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
updatedAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Last update`),
description: i18nLabel(msg`Last time the record was changed`),
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIEditable: false,
defaultValue: 'now',
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
deletedAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'deletedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Deleted at`),
description: i18nLabel(msg`Date when the record was deleted`),
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIEditable: false,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
createdBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'createdBy',
type: FieldMetadataType.ACTOR,
label: i18nLabel(msg`Created by`),
description: i18nLabel(msg`The creator of the record`),
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIEditable: false,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
updatedBy: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'updatedBy',
type: FieldMetadataType.ACTOR,
label: i18nLabel(msg`Updated by`),
description: i18nLabel(
msg`The workspace member who last updated the record`,
),
icon: 'IconUserCircle',
isSystem: true,
isUIEditable: false,
isNullable: false,
defaultValue: {
source: "'MANUAL'",
name: "'System'",
workspaceMemberId: null,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
position: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'position',
type: FieldMetadataType.POSITION,
label: i18nLabel(msg`Position`),
description: i18nLabel(msg`List record position`),
icon: 'IconHierarchy2',
isSystem: true,
isNullable: false,
defaultValue: 0,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
name: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'name',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Name`),
description: i18nLabel(msg`The list name`),
icon: 'IconUsersGroup',
isNullable: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
members: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'members',
label: i18nLabel(msg`Members`),
description: i18nLabel(msg`People in this list`),
icon: 'IconUser',
isNullable: true,
targetObjectName: 'messageListMember',
targetFieldName: 'list',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
junctionTargetFieldUniversalIdentifier:
STANDARD_OBJECTS.messageListMember.fields.person.universalIdentifier,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
campaigns: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'campaigns',
label: i18nLabel(msg`Campaigns`),
description: i18nLabel(msg`Campaigns sent to this list`),
icon: 'IconSend',
isUIEditable: false,
isNullable: true,
targetObjectName: 'messageCampaign',
targetFieldName: 'list',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
timelineActivities: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'timelineActivities',
label: i18nLabel(msg`Events`),
description: i18nLabel(msg`Events linked to the list`),
icon: 'IconTimelineEvent',
isNullable: true,
targetObjectName: 'timelineActivity',
targetFieldName: 'targetMessageList',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'searchVector',
type: FieldMetadataType.TS_VECTOR,
label: i18nLabel(msg`Search vector`),
description: i18nLabel(msg`Field used for full-text search`),
icon: 'IconUsersGroup',
isSystem: true,
isNullable: true,
settings: {
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields(
SEARCH_FIELDS_FOR_MESSAGE_LIST,
),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -356,4 +356,31 @@ export const buildMessageParticipantStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
messageCampaign: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'messageCampaign',
label: i18nLabel(msg`Campaign`),
description: i18nLabel(
msg`The campaign this participant was a recipient of`,
),
icon: 'IconSend',
isNullable: true,
isUIEditable: false,
targetObjectName: 'messageCampaign',
targetFieldName: 'recipients',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'messageCampaignId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -334,4 +334,94 @@ export const buildMessageStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
messageCampaign: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'messageCampaign',
label: i18nLabel(msg`Campaign`),
description: i18nLabel(
msg`The campaign this message was sent as part of`,
),
icon: 'IconSend',
isNullable: true,
isUIEditable: false,
targetObjectName: 'messageCampaign',
targetFieldName: 'messages',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'messageCampaignId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
deliveryStatus: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'deliveryStatus',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Delivery status`),
description: i18nLabel(
msg`Per-recipient delivery status for campaign sends`,
),
icon: 'IconMailFast',
isNullable: true,
isUIEditable: false,
options: [
{
id: '6b189ac2-5054-45c0-a95b-25764e978d81',
value: 'QUEUED',
label: i18nLabel(msg`Queued`),
position: 0,
color: 'gray',
},
{
id: 'af7390a3-bd35-480b-9bc2-6f7d8589b3d2',
value: 'SENT',
label: i18nLabel(msg`Sent`),
position: 1,
color: 'green',
},
{
id: '39c934fc-01d7-48fa-9b79-8e19f75dab03',
value: 'FAILED',
label: i18nLabel(msg`Failed`),
position: 2,
color: 'red',
},
{
id: 'ade2b01f-8f10-43c6-ab3d-63b0d98ce40c',
value: 'BOUNCED',
label: i18nLabel(msg`Bounced`),
position: 3,
color: 'orange',
},
{
id: 'ae79b7bc-b416-4fd2-a366-ab8d91cb22da',
value: 'COMPLAINED',
label: i18nLabel(msg`Complained`),
position: 4,
color: 'purple',
},
{
id: 'c0d3f2a1-7e64-4b9a-8f21-1d5e6a7b8c90',
value: 'SKIPPED',
label: i18nLabel(msg`Skipped`),
position: 5,
color: 'yellow',
},
],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -1,4 +1,6 @@
import { msg } from '@lingui/core/macro';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { i18nLabel } from 'src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util';
import {
DateDisplayFormat,
@@ -490,6 +492,31 @@ export const buildPersonStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
listMemberships: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'listMemberships',
label: i18nLabel(msg`Lists`),
description: i18nLabel(msg`Lists the contact belongs to`),
icon: 'IconUsersGroup',
isUIEditable: true,
isNullable: true,
targetObjectName: 'messageListMember',
targetFieldName: 'person',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
junctionTargetFieldUniversalIdentifier:
STANDARD_OBJECTS.messageListMember.fields.list.universalIdentifier,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -536,6 +536,56 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
targetMessageList: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.MORPH_RELATION,
morphId: STANDARD_OBJECTS.timelineActivity.morphIds.targetMorphId.morphId,
fieldName: 'targetMessageList',
label: i18nLabel(msg`Target`),
description: i18nLabel(msg`Event target`),
icon: 'IconArrowUpRight',
isNullable: true,
isUIEditable: false,
targetObjectName: 'messageList',
targetFieldName: 'timelineActivities',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetMessageListId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
targetMessageCampaign: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.MORPH_RELATION,
morphId: STANDARD_OBJECTS.timelineActivity.morphIds.targetMorphId.morphId,
fieldName: 'targetMessageCampaign',
label: i18nLabel(msg`Target`),
description: i18nLabel(msg`Event target`),
icon: 'IconArrowUpRight',
isNullable: true,
isUIEditable: false,
targetObjectName: 'messageCampaign',
targetFieldName: 'timelineActivities',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'targetMessageCampaignId',
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVector: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -40,6 +40,7 @@ export type CreateStandardMorphOrRelationFieldContext<
settings: FieldMetadataSettings<F>;
options?: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[] | null;
morphId: F extends FieldMetadataType.MORPH_RELATION ? string : null;
junctionTargetFieldUniversalIdentifier?: string;
};
export type CreateStandardRelationFieldArgs<
@@ -70,6 +71,7 @@ export const createStandardRelationFieldFlatMetadata = <
options: fieldOptions = null,
morphId,
type,
junctionTargetFieldUniversalIdentifier,
},
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
@@ -135,6 +137,11 @@ export const createStandardRelationFieldFlatMetadata = <
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
viewSortIds: [],
viewSortUniversalIdentifiers: [],
universalSettings: settings,
universalSettings: {
...settings,
...(junctionTargetFieldUniversalIdentifier && {
junctionTargetFieldUniversalIdentifier,
}),
},
};
};
@@ -35,7 +35,7 @@ const computeStandardViewObjectIds = <O extends AllStandardObjectName>({
}): StandardObjectViewIds<O> | undefined => {
const objectDefinition = STANDARD_OBJECTS[objectName];
if (!Object.prototype.hasOwnProperty.call(objectDefinition, 'views')) {
if (!('views' in objectDefinition)) {
return undefined;
}
@@ -10,6 +10,9 @@ import { buildCalendarEventParticipantStandardFlatIndexMetadatas } from 'src/eng
import { buildCallRecordingStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-call-recording-standard-flat-index-metadata.util';
import { buildCompanyStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-company-standard-flat-index-metadata.util';
import { buildDashboardStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-dashboard-standard-flat-index-metadata.util';
import { buildMessageCampaignStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-campaign-standard-flat-index-metadata.util';
import { buildMessageListStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-list-standard-flat-index-metadata.util';
import { buildMessageListMemberStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-list-member-standard-flat-index-metadata.util';
import { buildMessageChannelMessageAssociationMessageFolderStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-channel-message-association-message-folder-standard-flat-index-metadata.util';
import { buildMessageChannelMessageAssociationStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-channel-message-association-standard-flat-index-metadata.util';
import { buildMessageParticipantStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-participant-standard-flat-index-metadata.util';
@@ -42,6 +45,9 @@ const STANDARD_FLAT_INDEX_METADATA_BUILDERS_BY_OBJECT_NAME = {
callRecording: buildCallRecordingStandardFlatIndexMetadatas,
company: buildCompanyStandardFlatIndexMetadatas,
dashboard: buildDashboardStandardFlatIndexMetadatas,
messageCampaign: buildMessageCampaignStandardFlatIndexMetadatas,
messageList: buildMessageListStandardFlatIndexMetadatas,
messageListMember: buildMessageListMemberStandardFlatIndexMetadatas,
message: buildMessageStandardFlatIndexMetadatas,
messageChannelMessageAssociation:
buildMessageChannelMessageAssociationStandardFlatIndexMetadatas,
@@ -0,0 +1,57 @@
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { type AllStandardObjectIndexName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-index-name.type';
import {
type CreateStandardIndexArgs,
createStandardIndexFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/index/create-standard-index-flat-metadata.util';
export const buildMessageCampaignStandardFlatIndexMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<CreateStandardIndexArgs<'messageCampaign'>, 'context'>): Record<
AllStandardObjectIndexName<'messageCampaign'>,
FlatIndexMetadata
> => ({
unsubscribeTopicIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'unsubscribeTopicIdIndex',
relatedFieldNames: ['unsubscribeTopicId'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
listIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'listIdIndex',
relatedFieldNames: ['list'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
searchVectorGinIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'searchVectorGinIndex',
relatedFieldNames: ['searchVector'],
indexType: IndexType.GIN,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -0,0 +1,45 @@
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type AllStandardObjectIndexName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-index-name.type';
import {
type CreateStandardIndexArgs,
createStandardIndexFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/index/create-standard-index-flat-metadata.util';
export const buildMessageListMemberStandardFlatIndexMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<CreateStandardIndexArgs<'messageListMember'>, 'context'>): Record<
AllStandardObjectIndexName<'messageListMember'>,
FlatIndexMetadata
> => ({
listIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'listIdIndex',
relatedFieldNames: ['list'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
personListUniqueIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'personListUniqueIndex',
relatedFieldNames: ['person', 'list'],
isUnique: true,
indexWhereClause: '"deletedAt" IS NULL',
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -0,0 +1,33 @@
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { type AllStandardObjectIndexName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-index-name.type';
import {
type CreateStandardIndexArgs,
createStandardIndexFlatMetadata,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/index/create-standard-index-flat-metadata.util';
export const buildMessageListStandardFlatIndexMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<CreateStandardIndexArgs<'messageList'>, 'context'>): Record<
AllStandardObjectIndexName<'messageList'>,
FlatIndexMetadata
> => ({
searchVectorGinIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'searchVectorGinIndex',
relatedFieldNames: ['searchVector'],
indexType: IndexType.GIN,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -52,4 +52,16 @@ export const buildMessageParticipantStandardFlatIndexMetadatas = ({
twentyStandardApplicationId,
now,
}),
messageCampaignIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'messageCampaignIdIndex',
relatedFieldNames: ['messageCampaign'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -28,4 +28,16 @@ export const buildMessageStandardFlatIndexMetadatas = ({
twentyStandardApplicationId,
now,
}),
messageCampaignIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'messageCampaignIdIndex',
relatedFieldNames: ['messageCampaign'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -245,6 +245,96 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
twentyStandardApplicationId,
now,
}),
messageCampaign: ({
now,
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
dependencyFlatEntityMaps,
}: Omit<
CreateStandardObjectArgs<'messageCampaign'>,
'context' | 'objectName'
>) =>
createStandardObjectFlatMetadata({
objectName: 'messageCampaign',
dependencyFlatEntityMaps,
context: {
universalIdentifier:
STANDARD_OBJECTS.messageCampaign.universalIdentifier,
nameSingular: 'messageCampaign',
namePlural: 'messageCampaigns',
labelSingular: i18nLabel(msg`Campaign`),
labelPlural: i18nLabel(msg`Campaigns`),
description: i18nLabel(
msg`A bulk email send to an audience, with delivery stats`,
),
icon: 'IconSend',
isSystem: true,
isUICreatable: false,
labelIdentifierFieldMetadataName: 'subject',
},
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
now,
}),
messageList: ({
now,
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
dependencyFlatEntityMaps,
}: Omit<CreateStandardObjectArgs<'messageList'>, 'context' | 'objectName'>) =>
createStandardObjectFlatMetadata({
objectName: 'messageList',
dependencyFlatEntityMaps,
context: {
universalIdentifier: STANDARD_OBJECTS.messageList.universalIdentifier,
nameSingular: 'messageList',
namePlural: 'messageLists',
labelSingular: i18nLabel(msg`List`),
labelPlural: i18nLabel(msg`Lists`),
description: i18nLabel(msg`A hand-picked audience of people`),
icon: 'IconUsersGroup',
isSystem: true,
isSearchable: true,
labelIdentifierFieldMetadataName: 'name',
},
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
now,
}),
messageListMember: ({
now,
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
dependencyFlatEntityMaps,
}: Omit<
CreateStandardObjectArgs<'messageListMember'>,
'context' | 'objectName'
>) =>
createStandardObjectFlatMetadata({
objectName: 'messageListMember',
dependencyFlatEntityMaps,
context: {
universalIdentifier:
STANDARD_OBJECTS.messageListMember.universalIdentifier,
nameSingular: 'messageListMember',
namePlural: 'messageListMembers',
labelSingular: i18nLabel(msg`List Member`),
labelPlural: i18nLabel(msg`List Members`),
description: i18nLabel(msg`A person's membership in a list`),
icon: 'IconUser',
isSystem: true,
labelIdentifierFieldMetadataName: 'id',
},
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
now,
}),
messageChannelMessageAssociation: ({
now,
workspaceId,
@@ -4,9 +4,11 @@ export { STANDARD_CALENDAR_EVENT_PARTICIPANT_PAGE_LAYOUT_CONFIG } from './standa
export { STANDARD_CALL_RECORDING_PAGE_LAYOUT_CONFIG } from './standard-call-recording-page-layout.config';
export { STANDARD_COMPANY_PAGE_LAYOUT_CONFIG } from './standard-company-page-layout.config';
export { STANDARD_DASHBOARD_PAGE_LAYOUT_CONFIG } from './standard-dashboard-page-layout.config';
export { STANDARD_MESSAGE_CAMPAIGN_PAGE_LAYOUT_CONFIG } from './standard-message-campaign-page-layout.config';
export { STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_MESSAGE_FOLDER_PAGE_LAYOUT_CONFIG } from './standard-message-channel-message-association-message-folder-page-layout.config';
export { STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_PAGE_LAYOUT_CONFIG } from './standard-message-channel-message-association-page-layout.config';
export { STANDARD_MESSAGE_PARTICIPANT_PAGE_LAYOUT_CONFIG } from './standard-message-participant-page-layout.config';
export { STANDARD_MESSAGE_LIST_PAGE_LAYOUT_CONFIG } from './standard-message-list-page-layout.config';
export { STANDARD_MESSAGE_THREAD_PAGE_LAYOUT_CONFIG } from './standard-message-thread-page-layout.config';
export { STANDARD_NOTE_PAGE_LAYOUT_CONFIG } from './standard-note-page-layout.config';
export { STANDARD_OPPORTUNITY_PAGE_LAYOUT_CONFIG } from './standard-opportunity-page-layout.config';
@@ -0,0 +1,80 @@
import {
STANDARD_OBJECTS,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-shared/metadata';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
GRID_POSITIONS,
TAB_PROPS,
VERTICAL_LIST_LAYOUT_POSITIONS,
WIDGET_PROPS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import {
type StandardPageLayoutConfig,
type StandardPageLayoutTabConfig,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-page-layout-config.type';
const MESSAGE_CAMPAIGN_PAGE_TABS = {
home: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage.tabs
.home.universalIdentifier,
...TAB_PROPS.home,
widgets: {
fields: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
.tabs.home.widgets.fields.universalIdentifier,
...WIDGET_PROPS.fields,
},
list: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
.tabs.home.widgets.list.universalIdentifier,
title: 'List',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
fieldUniversalIdentifier:
STANDARD_OBJECTS.messageCampaign.fields.list.universalIdentifier,
},
recipients: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
.tabs.home.widgets.recipients.universalIdentifier,
title: 'Recipients',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FOURTH,
fieldUniversalIdentifier:
STANDARD_OBJECTS.messageCampaign.fields.recipients
.universalIdentifier,
},
messages: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
.tabs.home.widgets.messages.universalIdentifier,
title: 'Sent Messages',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FIFTH,
fieldUniversalIdentifier:
STANDARD_OBJECTS.messageCampaign.fields.messages.universalIdentifier,
},
},
},
} as const satisfies Record<string, StandardPageLayoutTabConfig>;
export const STANDARD_MESSAGE_CAMPAIGN_PAGE_LAYOUT_CONFIG = {
name: 'Default Campaign Layout',
type: PageLayoutType.RECORD_PAGE,
objectUniversalIdentifier:
STANDARD_OBJECTS.messageCampaign.universalIdentifier,
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageCampaignRecordPage
.universalIdentifier,
defaultTabUniversalIdentifier: null,
tabs: MESSAGE_CAMPAIGN_PAGE_TABS,
} as const satisfies StandardPageLayoutConfig;
@@ -0,0 +1,56 @@
import {
STANDARD_OBJECTS,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-shared/metadata';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
GRID_POSITIONS,
TAB_PROPS,
VERTICAL_LIST_LAYOUT_POSITIONS,
WIDGET_PROPS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import {
type StandardPageLayoutConfig,
type StandardPageLayoutTabConfig,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-config/standard-page-layout-config.type';
const MESSAGE_LIST_PAGE_TABS = {
home: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageListRecordPage.tabs.home
.universalIdentifier,
...TAB_PROPS.home,
widgets: {
fields: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageListRecordPage.tabs
.home.widgets.fields.universalIdentifier,
...WIDGET_PROPS.fields,
},
members: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageListRecordPage.tabs
.home.widgets.members.universalIdentifier,
title: 'Members',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
fieldUniversalIdentifier:
STANDARD_OBJECTS.messageList.fields.members.universalIdentifier,
},
},
},
} as const satisfies Record<string, StandardPageLayoutTabConfig>;
export const STANDARD_MESSAGE_LIST_PAGE_LAYOUT_CONFIG = {
name: 'Default List Layout',
type: PageLayoutType.RECORD_PAGE,
objectUniversalIdentifier: STANDARD_OBJECTS.messageList.universalIdentifier,
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.messageListRecordPage
.universalIdentifier,
defaultTabUniversalIdentifier: null,
tabs: MESSAGE_LIST_PAGE_TABS,
} as const satisfies StandardPageLayoutConfig;
@@ -52,6 +52,17 @@ const PERSON_PAGE_TABS = {
STANDARD_OBJECTS.person.fields.pointOfContactForOpportunities
.universalIdentifier,
},
listMemberships: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.personRecordPage.tabs.home
.widgets.listMemberships.universalIdentifier,
title: 'Lists',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FIFTH,
fieldUniversalIdentifier:
STANDARD_OBJECTS.person.fields.listMemberships.universalIdentifier,
},
},
},
timeline: {