[Call Recording] Add standard object (#21158)

Adds **Call Recording** as a first-class standard object (Twenty's
flat-metadata
standard-object system), with a hidden junction to calendar events and a
backfill
command for existing workspaces. Everything is gated behind the
`IS_CALL_RECORDING_ENABLED` feature flag.

### What's included
- **`CallRecording`**: audio/video files, transcript, status, recording
policy,
timing, external bot/recording ids. Label identifier is
`meetingOccurrenceKey`.
- **`CallRecordingCalendarEventAssociation`**: hidden junction linking a
recording
to a calendar event (dedupes one bot to many subscribers of the same
meeting).
- Full metadata graph via the flat-metadata builders: fields, indexes,
views,
  view fields/groups, record page layout, and navigation items.
- **Metadata-only reverse relation** on `CalendarEvent`: present in
standard
metadata, omitted from the TS entity class to avoid expanding recursive
  nested-insert types.
- **Upgrade command (2.9.0)** backfilling active/suspended workspaces:
  - Creates the full graph; idempotent (skips when it already exists).
- Moves a colliding custom `callRecording` object aside to
`callRecordingOld`
    (numeric suffix if that name is also taken).
- Navigation items (commands) are flag-gated by `universalIdentifier`,
so a custom object
    reusing the name is never gated.

### QA
Run locally against existing workspaces (with and without a name
collision) and a
freshly created workspace:
- [x] Backfill, collision: custom `callRecording` renamed to
`callRecordingOld`;
  standard graph created.
- [x] Backfill, no collision: standard graph created; unrelated custom
object untouched.
- [x] Idempotent: re-run is a no-op, with no duplicate metadata and
counts unchanged.
- [x] New workspace via `init()` produces an identical graph to the
backfill
  (`universalIdentifier` set-diff = 0).
- [x] Label identifier (`meetingOccurrenceKey`) holds position 0 in
non-widget views.
- [x] Nav items gated behind the feature flag; collision-renamed
object's nav
  expression re-pointed to its new name.
- [x] Unit tests cover collision name resolution and nav-gating logic.
This commit is contained in:
nitin
2026-06-05 18:32:50 +05:30
committed by GitHub
parent a78b319877
commit e485b679ea
41 changed files with 3999 additions and 899 deletions
@@ -1787,6 +1787,7 @@ enum FeatureFlagKey {
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED
IS_SETTINGS_DISCOVERY_HERO_ENABLED
IS_CALL_RECORDING_ENABLED
IS_WORKFLOW_RUN_STEP_LOGS_ENABLED
}
@@ -1414,7 +1414,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_CALL_RECORDING_ENABLED' | 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED'
export interface WorkspaceUrls {
customUrl?: Scalars['String']
@@ -8812,6 +8812,7 @@ export const enumFeatureFlagKey = {
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const,
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' as const,
IS_SETTINGS_DISCOVERY_HERO_ENABLED: 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' as const,
IS_CALL_RECORDING_ENABLED: 'IS_CALL_RECORDING_ENABLED' as const,
IS_WORKFLOW_RUN_STEP_LOGS_ENABLED: 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED' as const
}
@@ -283,6 +283,7 @@ export type FeatureFlag = {
};
export enum FeatureFlagKey {
IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED',
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
@@ -1648,6 +1648,7 @@ export type FeatureFlag = {
};
export enum FeatureFlagKey {
IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED',
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
@@ -5,6 +5,7 @@ import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/w
import { AddInactiveGenericStandardFieldsCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000050000-add-inactive-generic-standard-fields.command';
import { MoveDemotedStandardFieldsToCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000040000-move-demoted-standard-fields-to-custom-application.command';
import { RenameConflictingCustomFieldsCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000045000-rename-conflicting-custom-fields.command';
import { SyncCallRecordingStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/2-10/2-10-workspace-command-1799000055000-sync-call-recording-standard-objects.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
@@ -24,6 +25,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
MoveDemotedStandardFieldsToCustomApplicationCommand,
RenameConflictingCustomFieldsCommand,
AddInactiveGenericStandardFieldsCommand,
SyncCallRecordingStandardObjectsCommand,
],
})
export class V2_10_UpgradeVersionCommandModule {}
@@ -0,0 +1,425 @@
import { Command } from 'nest-commander';
import {
STANDARD_OBJECTS,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { buildNavigationCommandMenuItemOperationsOrThrow } from 'src/database/commands/upgrade-version-command/2-10/utils/build-navigation-command-menu-item-operations-or-throw.util';
import {
buildCalendarEventFieldRenameUpdates,
buildCallRecordingObjectRenameUpdates,
} from 'src/database/commands/upgrade-version-command/2-10/utils/call-recording-name-collision.util';
import {
getExistingOrStandardFlatEntityOrThrow,
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 FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
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 FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.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 getUniversalIdentifiers = (
entitiesByName: Record<string, { universalIdentifier: string }>,
): string[] =>
Object.values(entitiesByName).map((entity) => entity.universalIdentifier);
const CALL_RECORDING_OBJECT_METADATA_UNIVERSAL_IDENTIFIERS = [
STANDARD_OBJECTS.callRecording.universalIdentifier,
];
const CALL_RECORDING_FIELD_METADATA_UNIVERSAL_IDENTIFIERS = [
...getUniversalIdentifiers(STANDARD_OBJECTS.callRecording.fields),
STANDARD_OBJECTS.calendarEvent.fields.recordingPreference.universalIdentifier,
STANDARD_OBJECTS.calendarEvent.fields.callRecordings.universalIdentifier,
];
const CALL_RECORDING_INDEX_UNIVERSAL_IDENTIFIERS = getUniversalIdentifiers(
STANDARD_OBJECTS.callRecording.indexes,
);
const CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIERS = [
STANDARD_OBJECTS.callRecording.views.allCallRecordings.universalIdentifier,
STANDARD_OBJECTS.callRecording.views.callRecordingRecordPageFields
.universalIdentifier,
];
const CALL_RECORDING_VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIERS =
getUniversalIdentifiers(
STANDARD_OBJECTS.callRecording.views.callRecordingRecordPageFields
.viewFieldGroups,
);
const CALL_RECORDING_VIEW_FIELD_UNIVERSAL_IDENTIFIERS = [
...getUniversalIdentifiers(
STANDARD_OBJECTS.callRecording.views.allCallRecordings.viewFields,
),
...getUniversalIdentifiers(
STANDARD_OBJECTS.callRecording.views.callRecordingRecordPageFields
.viewFields,
),
];
const CALL_RECORDING_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS = [
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage
.universalIdentifier,
];
const CALL_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIERS = [
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs.home
.universalIdentifier,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs
.timeline.universalIdentifier,
];
const CALL_RECORDING_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIERS = [
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs.home
.widgets.fields.universalIdentifier,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs
.timeline.widgets.timeline.universalIdentifier,
];
@RegisteredWorkspaceCommand('2.10.0', 1799000055000)
@Command({
name: 'upgrade:2-10:sync-call-recording-standard-objects',
description:
'Create the CallRecording standard metadata in existing workspaces',
})
export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
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,
flatIndexMaps,
flatViewMaps,
flatViewFieldMaps,
flatViewFieldGroupMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
flatCommandMenuItemMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatIndexMaps',
'flatViewMaps',
'flatViewFieldMaps',
'flatViewFieldGroupMaps',
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatCommandMenuItemMaps',
]);
const calendarEventObjectMetadata =
flatObjectMetadataMaps.byUniversalIdentifier[
STANDARD_OBJECTS.calendarEvent.universalIdentifier
];
if (!isDefined(calendarEventObjectMetadata)) {
throw new Error(
`calendarEvent object not found for workspace ${workspaceId}`,
);
}
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const now = new Date().toISOString();
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now,
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const objectMetadataRenameUpdates = buildCallRecordingObjectRenameUpdates({
flatObjectMetadataMaps,
now,
});
const fieldMetadataRenameUpdates = buildCalendarEventFieldRenameUpdates({
flatFieldMetadataMaps,
now,
});
const renamedCollisionObjectMetadatas = objectMetadataRenameUpdates.map(
(objectMetadata) => ({
universalIdentifier: objectMetadata.universalIdentifier,
nameSingular: objectMetadata.nameSingular,
}),
);
const callRecordingObjectMetadataForNavigation =
getExistingOrStandardFlatEntityOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatObjectMetadataMaps,
existingFlatEntityMaps: flatObjectMetadataMaps,
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
});
const navigationCommandMenuItemOperations =
buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: flatCommandMenuItemMaps,
objectMetadatasForNavigation: [
callRecordingObjectMetadataForNavigation,
],
applicationId: twentyStandardFlatApplication.id,
workspaceId,
now,
renamedCollisionObjectMetadatas,
});
const allFlatEntityOperationByMetadataName = {
objectMetadata: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatObjectMetadataMaps,
existingFlatEntityMaps: flatObjectMetadataMaps,
universalIdentifiers:
CALL_RECORDING_OBJECT_METADATA_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
fieldMetadata: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatFieldMetadata>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatFieldMetadataMaps,
existingFlatEntityMaps: flatFieldMetadataMaps,
universalIdentifiers:
CALL_RECORDING_FIELD_METADATA_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
index: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatIndexMetadata>({
standardFlatEntityMaps: standardAllFlatEntityMaps.flatIndexMaps,
existingFlatEntityMaps: flatIndexMaps,
universalIdentifiers: CALL_RECORDING_INDEX_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: getStandardFlatEntitiesToCreateOrThrow<FlatView>({
standardFlatEntityMaps: standardAllFlatEntityMaps.flatViewMaps,
existingFlatEntityMaps: flatViewMaps,
universalIdentifiers: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewFieldGroup: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatViewFieldGroup>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatViewFieldGroupMaps,
existingFlatEntityMaps: flatViewFieldGroupMaps,
universalIdentifiers:
CALL_RECORDING_VIEW_FIELD_GROUP_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatViewField>({
standardFlatEntityMaps: standardAllFlatEntityMaps.flatViewFieldMaps,
existingFlatEntityMaps: flatViewFieldMaps,
universalIdentifiers:
CALL_RECORDING_VIEW_FIELD_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayout: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatPageLayout>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatPageLayoutMaps,
existingFlatEntityMaps: flatPageLayoutMaps,
universalIdentifiers:
CALL_RECORDING_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatPageLayoutTab>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatPageLayoutTabMaps,
existingFlatEntityMaps: flatPageLayoutTabMaps,
universalIdentifiers:
CALL_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate:
getStandardFlatEntitiesToCreateOrThrow<FlatPageLayoutWidget>({
standardFlatEntityMaps:
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps,
existingFlatEntityMaps: flatPageLayoutWidgetMaps,
universalIdentifiers:
CALL_RECORDING_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIERS,
}),
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
commandMenuItem: navigationCommandMenuItemOperations,
};
const totalOperationCount =
objectMetadataRenameUpdates.length +
fieldMetadataRenameUpdates.length +
Object.values(allFlatEntityOperationByMetadataName).reduce(
(total, operations) =>
total +
operations.flatEntityToCreate.length +
operations.flatEntityToUpdate.length,
0,
);
if (totalOperationCount === 0) {
this.logger.log(
`CallRecording standard metadata already exists for workspace ${workspaceId}, skipping`,
);
return;
}
if (isDryRun) {
if (objectMetadataRenameUpdates.length > 0) {
this.logger.log(
`[DRY RUN] Would rename ${objectMetadataRenameUpdates.length} CallRecording object name collision(s) for workspace ${workspaceId}`,
);
}
if (fieldMetadataRenameUpdates.length > 0) {
this.logger.log(
`[DRY RUN] Would rename ${fieldMetadataRenameUpdates.length} calendarEvent field name collision(s) for workspace ${workspaceId}`,
);
}
this.logger.log(
`[DRY RUN] Would apply ${totalOperationCount} CallRecording standard metadata operations for workspace ${workspaceId}`,
);
return;
}
// Renames must commit before the create: a combined create + rename
// migration trips the namePlural unique index.
const collisionRenameMigrations = [
...objectMetadataRenameUpdates.map((flatObjectMetadata) => ({
applicationUniversalIdentifier:
flatObjectMetadata.applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName: {
objectMetadata: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatObjectMetadata],
},
},
})),
...fieldMetadataRenameUpdates.map((flatFieldMetadata) => ({
applicationUniversalIdentifier:
flatFieldMetadata.applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName: {
fieldMetadata: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatFieldMetadata],
},
},
})),
];
// Collisions can belong to different applications, so each rename runs as
// its own migration scoped to the colliding entity's application.
for (const {
applicationUniversalIdentifier,
allFlatEntityOperationByMetadataName,
} of collisionRenameMigrations) {
const renameResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier,
workspaceId,
allFlatEntityOperationByMetadataName,
},
);
if (renameResult.status === 'fail') {
throw new Error(
`Failed to rename CallRecording name collision for workspace ${workspaceId}: ${JSON.stringify(
renameResult,
null,
2,
)}`,
);
}
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
isSystemBuild: true,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
workspaceId,
allFlatEntityOperationByMetadataName,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new Error(
`Failed to create CallRecording standard objects for workspace ${workspaceId}: ${JSON.stringify(
validateAndBuildResult,
null,
2,
)}`,
);
}
this.logger.log(
`Applied ${totalOperationCount} CallRecording standard metadata operations for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,241 @@
import { v5 } from 'uuid';
import { buildNavigationCommandMenuItemOperationsOrThrow } from 'src/database/commands/upgrade-version-command/2-10/utils/build-navigation-command-menu-item-operations-or-throw.util';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import {
buildNavigationFlatCommandMenuItem,
NAVIGATION_COMMAND_UUID_NAMESPACE,
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
const APPLICATION_ID = 'application-id';
const WORKSPACE_ID = 'workspace-id';
const NOW = '2026-06-04T00:00:00.000Z';
const buildFlatCommandMenuItemMaps = (
flatCommandMenuItems: FlatCommandMenuItem[],
): FlatEntityMaps<FlatCommandMenuItem> => ({
byUniversalIdentifier: Object.fromEntries(
flatCommandMenuItems.map((flatCommandMenuItem) => [
flatCommandMenuItem.universalIdentifier,
flatCommandMenuItem,
]),
),
universalIdentifierById: Object.fromEntries(
flatCommandMenuItems.map((flatCommandMenuItem) => [
flatCommandMenuItem.id,
flatCommandMenuItem.universalIdentifier,
]),
),
universalIdentifiersByApplicationId: {},
});
const buildExistingNavigationItem = ({
objectId,
objectUniversalIdentifier,
nameSingular,
position,
}: {
objectId: string;
objectUniversalIdentifier: string;
nameSingular: string;
position: number;
}): FlatCommandMenuItem =>
buildNavigationFlatCommandMenuItem({
objectMetadata: {
id: objectId,
universalIdentifier: objectUniversalIdentifier,
nameSingular,
shortcut: null,
},
commandMenuItemId: `command-menu-item-${objectUniversalIdentifier}`,
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
position,
now: NOW,
});
describe('buildNavigationCommandMenuItemOperationsOrThrow', () => {
it('creates a navigation item for an active object that has no existing navigation', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([]),
objectMetadatasForNavigation: [objectMetadata],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [],
});
expect(result.flatEntityToCreate).toHaveLength(1);
expect(result.flatEntityToCreate[0].universalIdentifier).toBe(
v5('call-recording', NAVIGATION_COMMAND_UUID_NAMESPACE),
);
expect(result.flatEntityToCreate[0].position).toBe(0);
expect(result.flatEntityToCreate[0].payload).toEqual({
objectMetadataItemId: objectMetadata.id,
});
expect(result.flatEntityToUpdate).toHaveLength(0);
expect(result.flatEntityToDelete).toHaveLength(0);
});
it('uses the provided object id in the navigation payload', () => {
const objectMetadata = getFlatObjectMetadataMock({
id: 'existing-call-recording-object-id',
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([]),
objectMetadatasForNavigation: [objectMetadata],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [],
});
expect(result.flatEntityToCreate[0].payload).toEqual({
objectMetadataItemId: 'existing-call-recording-object-id',
});
});
it('does not create a navigation item for an inactive object', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
isActive: false,
});
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([]),
objectMetadatasForNavigation: [objectMetadata],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [],
});
expect(result.flatEntityToCreate).toHaveLength(0);
});
it('does not recreate a navigation item that already exists (idempotent)', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const existingNavigationItem = buildExistingNavigationItem({
objectId: objectMetadata.id,
objectUniversalIdentifier: 'call-recording',
nameSingular: 'callRecording',
position: 0,
});
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([
existingNavigationItem,
]),
objectMetadatasForNavigation: [objectMetadata],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [],
});
expect(result.flatEntityToCreate).toHaveLength(0);
});
it('positions the new navigation item after the highest existing position', () => {
const objectMetadata = getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const existingNavigationItems = [
buildExistingNavigationItem({
objectId: 'other-object-1-id',
objectUniversalIdentifier: 'other-object-1',
nameSingular: 'otherObjectOne',
position: 0,
}),
buildExistingNavigationItem({
objectId: 'other-object-2-id',
objectUniversalIdentifier: 'other-object-2',
nameSingular: 'otherObjectTwo',
position: 5,
}),
];
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps(
existingNavigationItems,
),
objectMetadatasForNavigation: [objectMetadata],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [],
});
expect(result.flatEntityToCreate[0].position).toBe(6);
});
it('rewrites the stale availability expression of a renamed object navigation item', () => {
const existingNavigationItem = buildExistingNavigationItem({
objectId: 'colliding-object-id',
objectUniversalIdentifier: 'colliding-object',
nameSingular: 'callRecording',
position: 0,
});
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([
existingNavigationItem,
]),
objectMetadatasForNavigation: [],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [
{
universalIdentifier: 'colliding-object',
nameSingular: 'callRecordingOld',
},
],
});
expect(result.flatEntityToUpdate).toHaveLength(1);
expect(result.flatEntityToUpdate[0].conditionalAvailabilityExpression).toBe(
'targetObjectReadPermissions.callRecordingOld',
);
expect(result.flatEntityToUpdate[0].updatedAt).toBe(NOW);
expect(result.flatEntityToCreate).toHaveLength(0);
});
it('does not produce an update when a renamed object has no existing navigation item', () => {
const result = buildNavigationCommandMenuItemOperationsOrThrow({
existingFlatCommandMenuItemMaps: buildFlatCommandMenuItemMaps([]),
objectMetadatasForNavigation: [],
applicationId: APPLICATION_ID,
workspaceId: WORKSPACE_ID,
now: NOW,
renamedCollisionObjectMetadatas: [
{
universalIdentifier: 'colliding-object',
nameSingular: 'callRecordingOld',
},
],
});
expect(result.flatEntityToUpdate).toHaveLength(0);
});
});
@@ -0,0 +1,467 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FieldMetadataType } from 'twenty-shared/types';
import {
buildCalendarEventFieldRenameUpdates,
buildCallRecordingObjectRenameUpdates,
findCalendarEventFieldNameCollisionsForCallRecording,
findCallRecordingObjectNameCollisions,
resolveAvailableOldCalendarEventFieldName,
resolveAvailableOldCallRecordingObjectNames,
} from 'src/database/commands/upgrade-version-command/2-10/utils/call-recording-name-collision.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
const NOW = '2026-06-04T00:00:00.000Z';
const CALENDAR_EVENT_OBJECT_METADATA_ID = 'calendar-event-object-metadata-id';
const buildFlatObjectMetadataMaps = (
flatObjectMetadatas: FlatObjectMetadata[],
): FlatEntityMaps<FlatObjectMetadata> => ({
byUniversalIdentifier: Object.fromEntries(
flatObjectMetadatas.map((flatObjectMetadata) => [
flatObjectMetadata.universalIdentifier,
flatObjectMetadata,
]),
),
universalIdentifierById: Object.fromEntries(
flatObjectMetadatas.map((flatObjectMetadata) => [
flatObjectMetadata.id,
flatObjectMetadata.universalIdentifier,
]),
),
universalIdentifiersByApplicationId: {},
});
const buildFlatFieldMetadataMaps = (
flatFieldMetadatas: FlatFieldMetadata[],
): FlatEntityMaps<FlatFieldMetadata> => ({
byUniversalIdentifier: Object.fromEntries(
flatFieldMetadatas.map((flatFieldMetadata) => [
flatFieldMetadata.universalIdentifier,
flatFieldMetadata,
]),
),
universalIdentifierById: Object.fromEntries(
flatFieldMetadatas.map((flatFieldMetadata) => [
flatFieldMetadata.id,
flatFieldMetadata.universalIdentifier,
]),
),
universalIdentifiersByApplicationId: {},
});
const getCalendarEventFieldMetadataMock = (
overrides: Pick<FlatFieldMetadata, 'name' | 'universalIdentifier'> &
Partial<FlatFieldMetadata>,
): FlatFieldMetadata =>
getFlatFieldMetadataMock({
objectMetadataId: CALENDAR_EVENT_OBJECT_METADATA_ID,
objectMetadataUniversalIdentifier:
STANDARD_OBJECTS.calendarEvent.universalIdentifier,
type: FieldMetadataType.TEXT,
label: overrides.name,
...overrides,
});
describe('findCallRecordingObjectNameCollisions', () => {
it('returns an empty array when no object uses the callRecording name', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'unrelated-object',
nameSingular: 'invoice',
namePlural: 'invoices',
}),
]);
expect(findCallRecordingObjectNameCollisions(maps)).toEqual([]);
});
it('returns the custom object whose singular name collides', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding-singular',
nameSingular: 'callRecording',
namePlural: 'myRecordings',
}),
]);
expect(
findCallRecordingObjectNameCollisions(maps).map(
(object) => object.universalIdentifier,
),
).toEqual(['colliding-singular']);
});
it('returns the custom object whose plural name collides', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding-plural',
nameSingular: 'myRecording',
namePlural: 'callRecordings',
}),
]);
expect(
findCallRecordingObjectNameCollisions(maps).map(
(object) => object.universalIdentifier,
),
).toEqual(['colliding-plural']);
});
it('returns every colliding object when singular and plural collide on different objects', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding-singular',
nameSingular: 'callRecording',
namePlural: 'myRecordings',
}),
getFlatObjectMetadataMock({
universalIdentifier: 'colliding-plural',
nameSingular: 'myRecording',
namePlural: 'callRecordings',
}),
]);
expect(
findCallRecordingObjectNameCollisions(maps)
.map((object) => object.universalIdentifier)
.sort(),
).toEqual(['colliding-plural', 'colliding-singular']);
});
it('excludes the standard callRecording object itself', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
nameSingular: 'callRecording',
namePlural: 'callRecordings',
isCustom: false,
isSystem: true,
}),
]);
expect(findCallRecordingObjectNameCollisions(maps)).toEqual([]);
});
});
describe('resolveAvailableOldCallRecordingObjectNames', () => {
it('returns the unsuffixed Old names when none are taken', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
}),
]);
expect(resolveAvailableOldCallRecordingObjectNames(maps)).toEqual({
nameSingular: 'callRecordingOld',
namePlural: 'callRecordingsOld',
labelSingular: 'Call Recording (Old)',
labelPlural: 'Call Recordings (Old)',
});
});
it('advances to the next discriminator when the Old name is already taken', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
}),
getFlatObjectMetadataMock({
universalIdentifier: 'old-already-taken',
nameSingular: 'callRecordingOld',
namePlural: 'callRecordingsOld',
}),
]);
expect(resolveAvailableOldCallRecordingObjectNames(maps)).toEqual({
nameSingular: 'callRecordingOld2',
namePlural: 'callRecordingsOld2',
labelSingular: 'Call Recording (Old) 2',
labelPlural: 'Call Recordings (Old) 2',
});
});
it('skips Old names provided as already reserved', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
}),
]);
expect(
resolveAvailableOldCallRecordingObjectNames(
maps,
new Set(['callRecordingOld', 'callRecordingsOld']),
),
).toEqual({
nameSingular: 'callRecordingOld2',
namePlural: 'callRecordingsOld2',
labelSingular: 'Call Recording (Old) 2',
labelPlural: 'Call Recordings (Old) 2',
});
});
it('skips a discriminator when only the plural Old name is taken', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'plural-old-taken',
nameSingular: 'unrelatedSingular',
namePlural: 'callRecordingsOld',
}),
]);
expect(resolveAvailableOldCallRecordingObjectNames(maps)).toEqual({
nameSingular: 'callRecordingOld2',
namePlural: 'callRecordingsOld2',
labelSingular: 'Call Recording (Old) 2',
labelPlural: 'Call Recordings (Old) 2',
});
});
it('throws when every candidate Old name is taken', () => {
const blockedSingularNames = Array.from({ length: 100 }, (_, attempt) => {
const discriminator = attempt === 0 ? '' : `${attempt + 1}`;
return `callRecordingOld${discriminator}`;
});
const maps = buildFlatObjectMetadataMaps(
blockedSingularNames.map((nameSingular, index) =>
getFlatObjectMetadataMock({
universalIdentifier: `taken-${index}`,
nameSingular,
namePlural: `${nameSingular}Filler`,
}),
),
);
expect(() => resolveAvailableOldCallRecordingObjectNames(maps)).toThrow(
'Could not find an available callRecordingOld name after 100 attempts',
);
});
});
describe('buildCallRecordingObjectRenameUpdates', () => {
it('builds a migration update for a colliding object name', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
}),
]);
expect(
buildCallRecordingObjectRenameUpdates({
flatObjectMetadataMaps: maps,
now: NOW,
}),
).toMatchObject([
{
universalIdentifier: 'colliding',
nameSingular: 'callRecordingOld',
namePlural: 'callRecordingsOld',
labelSingular: 'Call Recording (Old)',
labelPlural: 'Call Recordings (Old)',
isLabelSyncedWithName: false,
updatedAt: NOW,
},
]);
});
it('reserves generated names across multiple object updates', () => {
const maps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'colliding-singular',
nameSingular: 'callRecording',
namePlural: 'myRecordings',
}),
getFlatObjectMetadataMock({
universalIdentifier: 'colliding-plural',
nameSingular: 'myRecording',
namePlural: 'callRecordings',
}),
]);
expect(
buildCallRecordingObjectRenameUpdates({
flatObjectMetadataMaps: maps,
now: NOW,
}).map((objectMetadata) => objectMetadata.nameSingular),
).toEqual(['callRecordingOld', 'callRecordingOld2']);
});
});
describe('findCalendarEventFieldNameCollisionsForCallRecording', () => {
it('finds calendarEvent fields that use a name reserved by CallRecording', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-recording-preference',
name: 'recordingPreference',
label: 'Recording Preference',
}),
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-call-recordings',
name: 'callRecordings',
label: 'Call Recordings',
}),
]);
expect(
findCalendarEventFieldNameCollisionsForCallRecording(maps).map(
(fieldMetadata) => fieldMetadata.universalIdentifier,
),
).toEqual(['colliding-recording-preference', 'colliding-call-recordings']);
});
it('excludes the CallRecording standard calendarEvent fields', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier:
STANDARD_OBJECTS.calendarEvent.fields.recordingPreference
.universalIdentifier,
name: 'recordingPreference',
}),
getCalendarEventFieldMetadataMock({
universalIdentifier:
STANDARD_OBJECTS.calendarEvent.fields.callRecordings
.universalIdentifier,
name: 'callRecordings',
}),
]);
expect(findCalendarEventFieldNameCollisionsForCallRecording(maps)).toEqual(
[],
);
});
it('excludes fields on objects other than calendarEvent', () => {
const maps = buildFlatFieldMetadataMaps([
getFlatFieldMetadataMock({
universalIdentifier: 'other-object-field',
objectMetadataId: 'other-object-id',
objectMetadataUniversalIdentifier: 'other-object',
type: FieldMetadataType.TEXT,
name: 'recordingPreference',
label: 'Recording Preference',
}),
]);
expect(findCalendarEventFieldNameCollisionsForCallRecording(maps)).toEqual(
[],
);
});
});
describe('resolveAvailableOldCalendarEventFieldName', () => {
it('returns the unsuffixed Old field name when it is available', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-field',
name: 'recordingPreference',
}),
]);
expect(
resolveAvailableOldCalendarEventFieldName({
flatFieldMetadataMaps: maps,
originalFieldName: 'recordingPreference',
}),
).toBe('recordingPreferenceOld');
});
it('advances to the next discriminator when the Old field name is taken', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-field',
name: 'recordingPreference',
}),
getCalendarEventFieldMetadataMock({
universalIdentifier: 'old-field-name-taken',
name: 'recordingPreferenceOld',
}),
]);
expect(
resolveAvailableOldCalendarEventFieldName({
flatFieldMetadataMaps: maps,
originalFieldName: 'recordingPreference',
}),
).toBe('recordingPreferenceOld2');
});
it('skips Old field names provided as already reserved', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-field',
name: 'callRecordings',
}),
]);
expect(
resolveAvailableOldCalendarEventFieldName({
flatFieldMetadataMaps: maps,
originalFieldName: 'callRecordings',
additionalTakenNames: new Set(['callRecordingsOld']),
}),
).toBe('callRecordingsOld2');
});
});
describe('buildCalendarEventFieldRenameUpdates', () => {
it('builds a migration update for a colliding calendarEvent field name', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-field',
name: 'recordingPreference',
label: 'Recording Preference',
}),
]);
expect(
buildCalendarEventFieldRenameUpdates({
flatFieldMetadataMaps: maps,
now: NOW,
}),
).toMatchObject([
{
universalIdentifier: 'colliding-field',
name: 'recordingPreferenceOld',
label: 'Recording Preference (Old)',
isLabelSyncedWithName: false,
updatedAt: NOW,
},
]);
});
it('reserves generated names across multiple field updates', () => {
const maps = buildFlatFieldMetadataMaps([
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-field-one',
name: 'recordingPreference',
label: 'Recording Preference',
}),
getCalendarEventFieldMetadataMock({
universalIdentifier: 'colliding-field-two',
name: 'recordingPreference',
label: 'Recording Preference',
}),
]);
expect(
buildCalendarEventFieldRenameUpdates({
flatFieldMetadataMaps: maps,
now: NOW,
}).map((fieldMetadata) => fieldMetadata.name),
).toEqual(['recordingPreferenceOld', 'recordingPreferenceOld2']);
});
});
@@ -0,0 +1,177 @@
import {
getExistingOrStandardFlatEntityOrThrow,
getStandardFlatEntitiesToCreateOrThrow,
} from 'src/database/commands/upgrade-version-command/2-10/utils/get-standard-flat-entities-to-create-or-throw.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
const buildFlatObjectMetadataMaps = (
flatObjectMetadatas: FlatObjectMetadata[],
): FlatEntityMaps<FlatObjectMetadata> => ({
byUniversalIdentifier: Object.fromEntries(
flatObjectMetadatas.map((flatObjectMetadata) => [
flatObjectMetadata.universalIdentifier,
flatObjectMetadata,
]),
),
universalIdentifierById: Object.fromEntries(
flatObjectMetadatas.map((flatObjectMetadata) => [
flatObjectMetadata.id,
flatObjectMetadata.universalIdentifier,
]),
),
universalIdentifiersByApplicationId: {},
});
describe('getStandardFlatEntitiesToCreateOrThrow', () => {
it('returns the standard entity when it is absent from the existing maps', () => {
const standardEntity = getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const standardFlatEntityMaps = buildFlatObjectMetadataMaps([
standardEntity,
]);
const existingFlatEntityMaps = buildFlatObjectMetadataMaps([]);
expect(
getStandardFlatEntitiesToCreateOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps,
existingFlatEntityMaps,
universalIdentifiers: ['call-recording'],
}),
).toEqual([standardEntity]);
});
it('skips an entity that already exists in the existing maps (idempotent re-run)', () => {
const standardEntity = getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const standardFlatEntityMaps = buildFlatObjectMetadataMaps([
standardEntity,
]);
const existingFlatEntityMaps = buildFlatObjectMetadataMaps([
standardEntity,
]);
expect(
getStandardFlatEntitiesToCreateOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps,
existingFlatEntityMaps,
universalIdentifiers: ['call-recording'],
}),
).toEqual([]);
});
it('returns only the entities missing from the existing maps', () => {
const existingEntity = getFlatObjectMetadataMock({
universalIdentifier: 'already-synced',
nameSingular: 'alreadySynced',
namePlural: 'alreadySynceds',
});
const missingEntity = getFlatObjectMetadataMock({
universalIdentifier: 'not-yet-synced',
nameSingular: 'notYetSynced',
namePlural: 'notYetSynceds',
});
const standardFlatEntityMaps = buildFlatObjectMetadataMaps([
existingEntity,
missingEntity,
]);
const existingFlatEntityMaps = buildFlatObjectMetadataMaps([
existingEntity,
]);
expect(
getStandardFlatEntitiesToCreateOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps,
existingFlatEntityMaps,
universalIdentifiers: ['already-synced', 'not-yet-synced'],
}),
).toEqual([missingEntity]);
});
it('returns an empty array when no universal identifiers are requested', () => {
const standardFlatEntityMaps = buildFlatObjectMetadataMaps([
getFlatObjectMetadataMock({
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
}),
]);
expect(
getStandardFlatEntitiesToCreateOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps,
existingFlatEntityMaps: buildFlatObjectMetadataMaps([]),
universalIdentifiers: [],
}),
).toEqual([]);
});
it('throws when a requested identifier is missing from the standard maps', () => {
expect(() =>
getStandardFlatEntitiesToCreateOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps: buildFlatObjectMetadataMaps([]),
existingFlatEntityMaps: buildFlatObjectMetadataMaps([]),
universalIdentifiers: ['missing-from-standard'],
}),
).toThrow('Could not find standard entity missing-from-standard');
});
});
describe('getExistingOrStandardFlatEntityOrThrow', () => {
it('returns the existing entity when one already exists', () => {
const standardEntity = getFlatObjectMetadataMock({
id: 'standard-id',
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
const existingEntity = getFlatObjectMetadataMock({
id: 'existing-id',
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
expect(
getExistingOrStandardFlatEntityOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps: buildFlatObjectMetadataMaps([standardEntity]),
existingFlatEntityMaps: buildFlatObjectMetadataMaps([existingEntity]),
universalIdentifier: 'call-recording',
}),
).toBe(existingEntity);
});
it('returns the standard entity when the entity is missing from existing maps', () => {
const standardEntity = getFlatObjectMetadataMock({
id: 'standard-id',
universalIdentifier: 'call-recording',
nameSingular: 'callRecording',
namePlural: 'callRecordings',
});
expect(
getExistingOrStandardFlatEntityOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps: buildFlatObjectMetadataMaps([standardEntity]),
existingFlatEntityMaps: buildFlatObjectMetadataMaps([]),
universalIdentifier: 'call-recording',
}),
).toBe(standardEntity);
});
it('throws when the entity is absent from both maps', () => {
expect(() =>
getExistingOrStandardFlatEntityOrThrow<FlatObjectMetadata>({
standardFlatEntityMaps: buildFlatObjectMetadataMaps([]),
existingFlatEntityMaps: buildFlatObjectMetadataMaps([]),
universalIdentifier: 'missing-from-standard',
}),
).toThrow('Could not find standard entity missing-from-standard');
});
});
@@ -0,0 +1,98 @@
import { isDefined } from 'twenty-shared/utils';
import { v4, v5 } from 'uuid';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import {
buildNavigationConditionalAvailabilityExpression,
buildNavigationFlatCommandMenuItem,
NAVIGATION_COMMAND_UUID_NAMESPACE,
} from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatEntityToCreateDeleteUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-to-create-delete-update.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const buildNavigationCommandMenuItemOperationsOrThrow = ({
existingFlatCommandMenuItemMaps,
objectMetadatasForNavigation,
applicationId,
workspaceId,
now,
renamedCollisionObjectMetadatas,
}: {
existingFlatCommandMenuItemMaps: FlatEntityMaps<FlatCommandMenuItem>;
objectMetadatasForNavigation: FlatObjectMetadata[];
applicationId: string;
workspaceId: string;
now: string;
renamedCollisionObjectMetadatas: {
universalIdentifier: string;
nameSingular: string;
}[];
}): FlatEntityToCreateDeleteUpdate<'commandMenuItem'> => {
const flatEntityToCreate: FlatCommandMenuItem[] = [];
const flatEntityToUpdate: FlatCommandMenuItem[] = [];
let nextPosition =
Object.values(existingFlatCommandMenuItemMaps.byUniversalIdentifier)
.filter(isDefined)
.reduce(
(maxPosition, commandMenuItem) =>
Math.max(maxPosition, commandMenuItem.position),
-1,
) + 1;
for (const objectMetadata of objectMetadatasForNavigation) {
const commandMenuItemUniversalIdentifier = v5(
objectMetadata.universalIdentifier,
NAVIGATION_COMMAND_UUID_NAMESPACE,
);
if (
!objectMetadata.isActive ||
isDefined(
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
commandMenuItemUniversalIdentifier
],
)
) {
continue;
}
flatEntityToCreate.push(
buildNavigationFlatCommandMenuItem({
objectMetadata,
commandMenuItemId: v4(),
applicationId,
workspaceId,
position: nextPosition++,
now,
}),
);
}
for (const renamedCollisionObjectMetadata of renamedCollisionObjectMetadatas) {
const renamedNavigationCommandMenuItemUniversalIdentifier = v5(
renamedCollisionObjectMetadata.universalIdentifier,
NAVIGATION_COMMAND_UUID_NAMESPACE,
);
const staleNavigationCommandMenuItem =
existingFlatCommandMenuItemMaps.byUniversalIdentifier[
renamedNavigationCommandMenuItemUniversalIdentifier
];
if (isDefined(staleNavigationCommandMenuItem)) {
flatEntityToUpdate.push({
...staleNavigationCommandMenuItem,
conditionalAvailabilityExpression:
buildNavigationConditionalAvailabilityExpression({
universalIdentifier:
renamedCollisionObjectMetadata.universalIdentifier,
nameSingular: renamedCollisionObjectMetadata.nameSingular,
}),
updatedAt: now,
});
}
}
return { flatEntityToCreate, flatEntityToDelete: [], flatEntityToUpdate };
};
@@ -0,0 +1,204 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
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';
const CALL_RECORDING_NAME_SINGULAR = 'callRecording';
const CALL_RECORDING_NAME_PLURAL = 'callRecordings';
const CALL_RECORDING_OLD_NAME_SINGULAR = 'callRecordingOld';
const CALL_RECORDING_OLD_NAME_PLURAL = 'callRecordingsOld';
const CALENDAR_EVENT_RECORDING_PREFERENCE_FIELD_NAME = 'recordingPreference';
const CALENDAR_EVENT_CALL_RECORDINGS_FIELD_NAME = 'callRecordings';
const FIELD_OLD_NAME_SUFFIX = 'Old';
const FIELD_OLD_LABEL_SUFFIX = ' (Old)';
const MAX_OLD_NAME_ATTEMPTS = 100;
const CALL_RECORDING_CALENDAR_EVENT_FIELD_NAMES = [
CALENDAR_EVENT_RECORDING_PREFERENCE_FIELD_NAME,
CALENDAR_EVENT_CALL_RECORDINGS_FIELD_NAME,
];
const CALL_RECORDING_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIERS =
new Set<string>([
STANDARD_OBJECTS.calendarEvent.fields.recordingPreference
.universalIdentifier,
STANDARD_OBJECTS.calendarEvent.fields.callRecordings.universalIdentifier,
]);
export const findCallRecordingObjectNameCollisions = (
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
): FlatObjectMetadata[] =>
Object.values(flatObjectMetadataMaps.byUniversalIdentifier).filter(
(flatObjectMetadata): flatObjectMetadata is FlatObjectMetadata =>
isDefined(flatObjectMetadata) &&
flatObjectMetadata.universalIdentifier !==
STANDARD_OBJECTS.callRecording.universalIdentifier &&
[flatObjectMetadata.nameSingular, flatObjectMetadata.namePlural].some(
(name) =>
name === CALL_RECORDING_NAME_SINGULAR ||
name === CALL_RECORDING_NAME_PLURAL,
),
);
export const resolveAvailableOldCallRecordingObjectNames = (
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
additionalTakenNames: ReadonlySet<string> = new Set(),
): {
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
} => {
const takenNames = new Set([
...Object.values(flatObjectMetadataMaps.byUniversalIdentifier)
.filter(isDefined)
.flatMap((flatObjectMetadata) => [
flatObjectMetadata.nameSingular,
flatObjectMetadata.namePlural,
]),
...additionalTakenNames,
]);
for (let attempt = 0; attempt < MAX_OLD_NAME_ATTEMPTS; attempt++) {
const discriminator = attempt === 0 ? '' : `${attempt + 1}`;
const nameSingular = `${CALL_RECORDING_OLD_NAME_SINGULAR}${discriminator}`;
const namePlural = `${CALL_RECORDING_OLD_NAME_PLURAL}${discriminator}`;
if (!takenNames.has(nameSingular) && !takenNames.has(namePlural)) {
const labelSuffix = discriminator === '' ? '' : ` ${discriminator}`;
return {
nameSingular,
namePlural,
labelSingular: `Call Recording (Old)${labelSuffix}`,
labelPlural: `Call Recordings (Old)${labelSuffix}`,
};
}
}
throw new Error(
`Could not find an available callRecordingOld name after ${MAX_OLD_NAME_ATTEMPTS} attempts`,
);
};
export const buildCallRecordingObjectRenameUpdates = ({
flatObjectMetadataMaps,
now,
}: {
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
now: string;
}): FlatObjectMetadata[] => {
const reservedOldNames = new Set<string>();
return findCallRecordingObjectNameCollisions(flatObjectMetadataMaps).map(
(collidingObjectMetadata) => {
const { nameSingular, namePlural, labelSingular, labelPlural } =
resolveAvailableOldCallRecordingObjectNames(
flatObjectMetadataMaps,
reservedOldNames,
);
reservedOldNames.add(nameSingular);
reservedOldNames.add(namePlural);
return {
...collidingObjectMetadata,
nameSingular,
namePlural,
labelSingular,
labelPlural,
isLabelSyncedWithName: false,
updatedAt: now,
};
},
);
};
export const findCalendarEventFieldNameCollisionsForCallRecording = (
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
): FlatFieldMetadata[] =>
Object.values(flatFieldMetadataMaps.byUniversalIdentifier).filter(
(flatFieldMetadata): flatFieldMetadata is FlatFieldMetadata =>
isDefined(flatFieldMetadata) &&
flatFieldMetadata.objectMetadataUniversalIdentifier ===
STANDARD_OBJECTS.calendarEvent.universalIdentifier &&
!CALL_RECORDING_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIERS.has(
flatFieldMetadata.universalIdentifier,
) &&
CALL_RECORDING_CALENDAR_EVENT_FIELD_NAMES.includes(
flatFieldMetadata.name,
),
);
export const resolveAvailableOldCalendarEventFieldName = ({
flatFieldMetadataMaps,
originalFieldName,
additionalTakenNames = new Set(),
}: {
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
originalFieldName: string;
additionalTakenNames?: ReadonlySet<string>;
}): string => {
const takenFieldNames = new Set([
...Object.values(flatFieldMetadataMaps.byUniversalIdentifier)
.filter(isDefined)
.filter(
(flatFieldMetadata) =>
flatFieldMetadata.objectMetadataUniversalIdentifier ===
STANDARD_OBJECTS.calendarEvent.universalIdentifier,
)
.map((flatFieldMetadata) => flatFieldMetadata.name),
...additionalTakenNames,
]);
for (let attempt = 0; attempt < MAX_OLD_NAME_ATTEMPTS; attempt++) {
const discriminator = attempt === 0 ? '' : `${attempt + 1}`;
const candidateName = `${originalFieldName}${FIELD_OLD_NAME_SUFFIX}${discriminator}`;
if (!takenFieldNames.has(candidateName)) {
return candidateName;
}
}
throw new Error(
`Could not find an available ${originalFieldName}Old name after ${MAX_OLD_NAME_ATTEMPTS} attempts`,
);
};
export const buildCalendarEventFieldRenameUpdates = ({
flatFieldMetadataMaps,
now,
}: {
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
now: string;
}): FlatFieldMetadata[] => {
const reservedOldFieldNames = new Set<string>();
return findCalendarEventFieldNameCollisionsForCallRecording(
flatFieldMetadataMaps,
).map((collidingFieldMetadata) => {
const name = resolveAvailableOldCalendarEventFieldName({
flatFieldMetadataMaps,
originalFieldName: collidingFieldMetadata.name,
additionalTakenNames: reservedOldFieldNames,
});
const oldNamePrefix = `${collidingFieldMetadata.name}${FIELD_OLD_NAME_SUFFIX}`;
const discriminator = name.slice(oldNamePrefix.length);
const labelSuffix =
discriminator === ''
? FIELD_OLD_LABEL_SUFFIX
: `${FIELD_OLD_LABEL_SUFFIX} ${discriminator}`;
reservedOldFieldNames.add(name);
return {
...collidingFieldMetadata,
name,
label: `${collidingFieldMetadata.label}${labelSuffix}`,
isLabelSyncedWithName: false,
updatedAt: now,
};
});
};
@@ -0,0 +1,62 @@
import { isDefined } from 'twenty-shared/utils';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
export const getStandardFlatEntitiesToCreateOrThrow = <
T extends SyncableFlatEntity,
>({
standardFlatEntityMaps,
existingFlatEntityMaps,
universalIdentifiers,
}: {
standardFlatEntityMaps: FlatEntityMaps<T>;
existingFlatEntityMaps: FlatEntityMaps<T>;
universalIdentifiers: string[];
}): T[] =>
universalIdentifiers.flatMap((universalIdentifier) => {
const standardFlatEntity =
standardFlatEntityMaps.byUniversalIdentifier[universalIdentifier];
if (!isDefined(standardFlatEntity)) {
throw new Error(`Could not find standard entity ${universalIdentifier}`);
}
if (
isDefined(
existingFlatEntityMaps.byUniversalIdentifier[universalIdentifier],
)
) {
return [];
}
return [standardFlatEntity];
});
export const getExistingOrStandardFlatEntityOrThrow = <
T extends SyncableFlatEntity,
>({
standardFlatEntityMaps,
existingFlatEntityMaps,
universalIdentifier,
}: {
standardFlatEntityMaps: FlatEntityMaps<T>;
existingFlatEntityMaps: FlatEntityMaps<T>;
universalIdentifier: string;
}): T => {
const existingFlatEntity =
existingFlatEntityMaps.byUniversalIdentifier[universalIdentifier];
if (isDefined(existingFlatEntity)) {
return existingFlatEntity;
}
const standardFlatEntity =
standardFlatEntityMaps.byUniversalIdentifier[universalIdentifier];
if (!isDefined(standardFlatEntity)) {
throw new Error(`Could not find standard entity ${universalIdentifier}`);
}
return standardFlatEntity;
};
@@ -1,8 +1,10 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { v5 } from 'uuid';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import {
buildNavigationConditionalAvailabilityExpression,
buildNavigationFlatCommandMenuItem,
NAVIGATION_INTERPOLATED_ICON,
NAVIGATION_INTERPOLATED_LABEL,
@@ -114,6 +116,21 @@ describe('buildNavigationFlatCommandMenuItem', () => {
);
});
it('should additionally gate conditionalAvailabilityExpression behind the feature flag for feature-flagged objects', () => {
const result = buildNavigationFlatCommandMenuItem({
...baseArgs,
objectMetadata: {
...baseObjectMetadata,
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
nameSingular: 'callRecording',
},
});
expect(result.conditionalAvailabilityExpression).toBe(
'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording',
);
});
it('should set isPinned to false', () => {
const result = buildNavigationFlatCommandMenuItem(baseArgs);
@@ -137,3 +154,34 @@ describe('buildNavigationFlatCommandMenuItem', () => {
expect(result.updatedAt).toBe('2026-01-01T00:00:00.000Z');
});
});
describe('buildNavigationConditionalAvailabilityExpression', () => {
it('gates the standard call recording object behind both the flag and read permission', () => {
expect(
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
nameSingular: 'callRecording',
}),
).toBe(
'featureFlags.IS_CALL_RECORDING_ENABLED and targetObjectReadPermissions.callRecording',
);
});
it('returns only the read-permission expression for non-gated objects', () => {
expect(
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: 'obj-universal-1',
nameSingular: 'person',
}),
).toBe('targetObjectReadPermissions.person');
});
it('does not gate a custom object that reuses the callRecording name', () => {
expect(
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: 'custom-object-universal-id',
nameSingular: 'callRecording',
}),
).toBe('targetObjectReadPermissions.callRecording');
});
});
@@ -1,3 +1,5 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v5 } from 'uuid';
@@ -16,6 +18,31 @@ export const NAVIGATION_INTERPOLATED_SHORT_LABEL =
export const NAVIGATION_INTERPOLATED_ICON =
'${navigateToObjectMetadataItem.icon}';
const NAVIGATION_FEATURE_FLAG_GATE_BY_OBJECT_UNIVERSAL_IDENTIFIER: Partial<
Record<string, FeatureFlagKey>
> = {
[STANDARD_OBJECTS.callRecording.universalIdentifier]:
FeatureFlagKey.IS_CALL_RECORDING_ENABLED,
};
export const buildNavigationConditionalAvailabilityExpression = ({
universalIdentifier,
nameSingular,
}: {
universalIdentifier: string;
nameSingular: string;
}): string => {
const targetObjectReadPermissionExpression = `targetObjectReadPermissions.${nameSingular}`;
const featureFlagGate =
NAVIGATION_FEATURE_FLAG_GATE_BY_OBJECT_UNIVERSAL_IDENTIFIER[
universalIdentifier
];
return isDefined(featureFlagGate)
? `featureFlags.${featureFlagGate} and ${targetObjectReadPermissionExpression}`
: targetObjectReadPermissionExpression;
};
export const buildNavigationFlatCommandMenuItem = ({
objectMetadata,
commandMenuItemId,
@@ -41,6 +68,12 @@ export const buildNavigationFlatCommandMenuItem = ({
NAVIGATION_COMMAND_UUID_NAMESPACE,
);
const conditionalAvailabilityExpression =
buildNavigationConditionalAvailabilityExpression({
universalIdentifier: objectMetadata.universalIdentifier,
nameSingular: objectMetadata.nameSingular,
});
return {
id: commandMenuItemId,
universalIdentifier,
@@ -54,7 +87,7 @@ export const buildNavigationFlatCommandMenuItem = ({
position,
isPinned: false,
availabilityType: CommandMenuItemAvailabilityType.GLOBAL,
conditionalAvailabilityExpression: `targetObjectReadPermissions.${objectMetadata.nameSingular}`,
conditionalAvailabilityExpression,
frontComponentId: null,
frontComponentUniversalIdentifier: null,
engineComponentKey: EngineComponentKey.NAVIGATION,
@@ -239,6 +239,7 @@ describe('WorkspaceEntityManager', () => {
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false,
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: false,
IS_SETTINGS_DISCOVERY_HERO_ENABLED: false,
IS_CALL_RECORDING_ENABLED: false,
IS_WORKFLOW_RUN_STEP_LOGS_ENABLED: false,
},
userWorkspaceRoleMap: {},
@@ -2,6 +2,7 @@ import {
STANDARD_BLOCKLIST_PAGE_LAYOUT_CONFIG,
STANDARD_CALENDAR_CHANNEL_EVENT_ASSOCIATION_PAGE_LAYOUT_CONFIG,
STANDARD_CALENDAR_EVENT_PARTICIPANT_PAGE_LAYOUT_CONFIG,
STANDARD_CALL_RECORDING_PAGE_LAYOUT_CONFIG,
STANDARD_COMPANY_PAGE_LAYOUT_CONFIG,
STANDARD_DASHBOARD_PAGE_LAYOUT_CONFIG,
STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_MESSAGE_FOLDER_PAGE_LAYOUT_CONFIG,
@@ -26,6 +27,7 @@ export const STANDARD_PAGE_LAYOUTS = {
STANDARD_CALENDAR_CHANNEL_EVENT_ASSOCIATION_PAGE_LAYOUT_CONFIG,
calendarEventParticipantRecordPage:
STANDARD_CALENDAR_EVENT_PARTICIPANT_PAGE_LAYOUT_CONFIG,
callRecordingRecordPage: STANDARD_CALL_RECORDING_PAGE_LAYOUT_CONFIG,
companyRecordPage: STANDARD_COMPANY_PAGE_LAYOUT_CONFIG,
messageChannelMessageAssociationRecordPage:
STANDARD_MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_PAGE_LAYOUT_CONFIG,
@@ -65,116 +65,95 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
},
"companyRecordPage": {
"callRecordingRecordPage": {
"id": "00000000-0000-0000-0000-000000000026",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000041",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000042",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000039",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000040",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000037",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000038",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000027",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000028",
},
"opportunities": {
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000029",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000030",
},
},
},
},
},
"companyRecordPage": {
"id": "00000000-0000-0000-0000-000000000031",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000046",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000047",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000044",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000045",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000042",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000043",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000032",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000033",
},
"opportunities": {
"id": "00000000-0000-0000-0000-000000000035",
},
"people": {
"id": "00000000-0000-0000-0000-000000000029",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000035",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000036",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000033",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000034",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000040",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000041",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000038",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000039",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000031",
"id": "00000000-0000-0000-0000-000000000036",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000032",
"id": "00000000-0000-0000-0000-000000000037",
},
},
},
},
},
"messageChannelMessageAssociationMessageFolderRecordPage": {
"id": "00000000-0000-0000-0000-000000000048",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000049",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000050",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000051",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000052",
},
},
},
},
},
"messageChannelMessageAssociationRecordPage": {
"id": "00000000-0000-0000-0000-000000000043",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000044",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000045",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000046",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000047",
},
},
},
},
},
"messageParticipantRecordPage": {
"id": "00000000-0000-0000-0000-000000000053",
"tabs": {
"home": {
@@ -195,17 +174,59 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
},
"messageThreadRecordPage": {
"messageChannelMessageAssociationRecordPage": {
"id": "00000000-0000-0000-0000-000000000048",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000049",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000050",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000051",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000052",
},
},
},
},
},
"messageParticipantRecordPage": {
"id": "00000000-0000-0000-0000-000000000058",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000059",
"widgets": {
"emailThread": {
"fields": {
"id": "00000000-0000-0000-0000-000000000060",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000061",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000062",
},
},
},
},
},
"messageThreadRecordPage": {
"id": "00000000-0000-0000-0000-000000000063",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000064",
"widgets": {
"emailThread": {
"id": "00000000-0000-0000-0000-000000000065",
},
},
},
},
},
"myFirstDashboard": {
@@ -243,299 +264,299 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
"noteRecordPage": {
"id": "00000000-0000-0000-0000-000000000061",
"id": "00000000-0000-0000-0000-000000000066",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000069",
"id": "00000000-0000-0000-0000-000000000074",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000070",
"id": "00000000-0000-0000-0000-000000000075",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000062",
"id": "00000000-0000-0000-0000-000000000067",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000063",
"id": "00000000-0000-0000-0000-000000000068",
},
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000064",
"id": "00000000-0000-0000-0000-000000000069",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000065",
"id": "00000000-0000-0000-0000-000000000070",
"widgets": {
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000066",
"id": "00000000-0000-0000-0000-000000000071",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000067",
"id": "00000000-0000-0000-0000-000000000072",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000068",
"id": "00000000-0000-0000-0000-000000000073",
},
},
},
},
},
"opportunityRecordPage": {
"id": "00000000-0000-0000-0000-000000000071",
"id": "00000000-0000-0000-0000-000000000076",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000087",
"id": "00000000-0000-0000-0000-000000000092",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000088",
"id": "00000000-0000-0000-0000-000000000093",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000085",
"id": "00000000-0000-0000-0000-000000000090",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000086",
"id": "00000000-0000-0000-0000-000000000091",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000083",
"id": "00000000-0000-0000-0000-000000000088",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000084",
"id": "00000000-0000-0000-0000-000000000089",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000072",
"id": "00000000-0000-0000-0000-000000000077",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000075",
"id": "00000000-0000-0000-0000-000000000080",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000073",
"id": "00000000-0000-0000-0000-000000000078",
},
"owner": {
"id": "00000000-0000-0000-0000-000000000076",
"id": "00000000-0000-0000-0000-000000000081",
},
"pointOfContact": {
"id": "00000000-0000-0000-0000-000000000074",
"id": "00000000-0000-0000-0000-000000000079",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000081",
"id": "00000000-0000-0000-0000-000000000086",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000082",
"id": "00000000-0000-0000-0000-000000000087",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000079",
"id": "00000000-0000-0000-0000-000000000084",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000080",
"id": "00000000-0000-0000-0000-000000000085",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000077",
"id": "00000000-0000-0000-0000-000000000082",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000078",
"id": "00000000-0000-0000-0000-000000000083",
},
},
},
},
},
"personRecordPage": {
"id": "00000000-0000-0000-0000-000000000089",
"id": "00000000-0000-0000-0000-000000000094",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000104",
"id": "00000000-0000-0000-0000-000000000109",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000105",
"id": "00000000-0000-0000-0000-000000000110",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000102",
"id": "00000000-0000-0000-0000-000000000107",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000103",
"id": "00000000-0000-0000-0000-000000000108",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000100",
"id": "00000000-0000-0000-0000-000000000105",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000101",
"id": "00000000-0000-0000-0000-000000000106",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000090",
"id": "00000000-0000-0000-0000-000000000095",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000092",
"id": "00000000-0000-0000-0000-000000000097",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000091",
"id": "00000000-0000-0000-0000-000000000096",
},
"pointOfContactForOpportunities": {
"id": "00000000-0000-0000-0000-000000000093",
"id": "00000000-0000-0000-0000-000000000098",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000098",
"id": "00000000-0000-0000-0000-000000000103",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000099",
"id": "00000000-0000-0000-0000-000000000104",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000096",
"id": "00000000-0000-0000-0000-000000000101",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000097",
"id": "00000000-0000-0000-0000-000000000102",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000094",
"id": "00000000-0000-0000-0000-000000000099",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000095",
"id": "00000000-0000-0000-0000-000000000100",
},
},
},
},
},
"taskRecordPage": {
"id": "00000000-0000-0000-0000-000000000106",
"id": "00000000-0000-0000-0000-000000000111",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000114",
"id": "00000000-0000-0000-0000-000000000119",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000115",
"id": "00000000-0000-0000-0000-000000000120",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000107",
"id": "00000000-0000-0000-0000-000000000112",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000108",
"id": "00000000-0000-0000-0000-000000000113",
},
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000109",
"id": "00000000-0000-0000-0000-000000000114",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000110",
"id": "00000000-0000-0000-0000-000000000115",
"widgets": {
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000111",
"id": "00000000-0000-0000-0000-000000000116",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000112",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000113",
},
},
},
},
},
"workflowAutomatedTriggerRecordPage": {
"id": "00000000-0000-0000-0000-000000000119",
"tabs": {
"home": {
"id": "00000000-0000-0000-0000-000000000120",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000121",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000122",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000123",
},
},
},
},
},
"workflowRecordPage": {
"id": "00000000-0000-0000-0000-000000000116",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000117",
"widgets": {
"workflow": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000118",
},
},
},
},
},
"workflowRunRecordPage": {
"id": "00000000-0000-0000-0000-000000000130",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000134",
"widgets": {
"workflowRun": {
"id": "00000000-0000-0000-0000-000000000135",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000131",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000132",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000133",
},
},
},
},
},
"workflowVersionRecordPage": {
"workflowAutomatedTriggerRecordPage": {
"id": "00000000-0000-0000-0000-000000000124",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000128",
"widgets": {
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000129",
},
},
},
"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-000000000127",
"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",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000138",
},
},
},
},
},
"workflowVersionRecordPage": {
"id": "00000000-0000-0000-0000-000000000129",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000133",
"widgets": {
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000134",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000130",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000131",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000132",
},
},
},
@@ -0,0 +1,139 @@
import {
STANDARD_OBJECTS,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
const WORKSPACE_ID = '20202020-1111-4111-8111-111111111111';
const TWENTY_STANDARD_APPLICATION_ID = '20202020-2222-4222-8222-222222222222';
const NOW = '2024-01-01T00:00:00.000Z';
describe('CallRecording standard metadata build', () => {
const { allFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now: NOW,
workspaceId: WORKSPACE_ID,
twentyStandardApplicationId: TWENTY_STANDARD_APPLICATION_ID,
});
it('builds the callRecording object', () => {
const { byUniversalIdentifier } = allFlatEntityMaps.flatObjectMetadataMaps;
expect(
byUniversalIdentifier[STANDARD_OBJECTS.callRecording.universalIdentifier],
).toBeDefined();
});
it('marks the callRecording object as system', () => {
const callRecording =
allFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
STANDARD_OBJECTS.callRecording.universalIdentifier
];
expect(callRecording?.isSystem).toBe(true);
});
it('links callRecording to a calendarEvent through a direct relation', () => {
const calendarEventField =
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
STANDARD_OBJECTS.callRecording.fields.calendarEvent.universalIdentifier
];
expect(calendarEventField).toBeDefined();
});
it('indexes the calendarEvent foreign key', () => {
const calendarEventIdIndex =
allFlatEntityMaps.flatIndexMaps.byUniversalIdentifier[
STANDARD_OBJECTS.callRecording.indexes.calendarEventIdIndex
.universalIdentifier
];
expect(calendarEventIdIndex).toBeDefined();
});
it('stores the per-event recording preference on calendarEvent', () => {
const recordingPreferenceField =
allFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
STANDARD_OBJECTS.calendarEvent.fields.recordingPreference
.universalIdentifier
];
expect(recordingPreferenceField).toBeDefined();
});
it('keeps the callRecording table view focused on its label identifier and status', () => {
const viewFieldFieldUniversalIdentifiers = Object.values(
allFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(viewField) =>
viewField.viewUniversalIdentifier ===
STANDARD_OBJECTS.callRecording.views.allCallRecordings
.universalIdentifier,
)
.map((viewField) => viewField.fieldMetadataUniversalIdentifier);
expect(viewFieldFieldUniversalIdentifiers).toHaveLength(3);
expect(viewFieldFieldUniversalIdentifiers).toEqual(
expect.arrayContaining([
STANDARD_OBJECTS.callRecording.fields.title.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.status.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.startedAt.universalIdentifier,
]),
);
});
it('uses the important callRecording detail fields on the record page', () => {
const viewFieldFieldUniversalIdentifiers = Object.values(
allFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(viewField) =>
viewField.viewUniversalIdentifier ===
STANDARD_OBJECTS.callRecording.views.callRecordingRecordPageFields
.universalIdentifier,
)
.map((viewField) => viewField.fieldMetadataUniversalIdentifier);
expect(viewFieldFieldUniversalIdentifiers).toHaveLength(8);
expect(viewFieldFieldUniversalIdentifiers).toEqual(
expect.arrayContaining([
STANDARD_OBJECTS.callRecording.fields.title.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.status.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.startedAt.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.endedAt.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.video.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.audio.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.transcript.universalIdentifier,
STANDARD_OBJECTS.callRecording.fields.summary.universalIdentifier,
]),
);
expect(viewFieldFieldUniversalIdentifiers).not.toContain(
STANDARD_OBJECTS.callRecording.fields.createdAt.universalIdentifier,
);
expect(viewFieldFieldUniversalIdentifiers).not.toContain(
STANDARD_OBJECTS.callRecording.fields.createdBy.universalIdentifier,
);
});
it('links the callRecording fields widget to its record-page fields view', () => {
const fieldsWidget =
allFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier[
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs
.home.widgets.fields.universalIdentifier
];
expect(fieldsWidget?.universalConfiguration).toMatchObject({
configurationType: WidgetConfigurationType.FIELDS,
viewUniversalIdentifier:
STANDARD_OBJECTS.callRecording.views.callRecordingRecordPageFields
.universalIdentifier,
});
});
});
@@ -10,6 +10,7 @@ import { buildBlocklistStandardFlatFieldMetadatas } from 'src/engine/workspace-m
import { buildCalendarChannelEventAssociationStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-calendar-channel-event-association-standard-flat-field-metadata.util';
import { buildCalendarEventParticipantStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-calendar-event-participant-standard-flat-field-metadata.util';
import { buildCalendarEventStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-calendar-event-standard-flat-field-metadata.util';
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 { buildMessageChannelMessageAssociationMessageFolderStandardFlatFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/compute-message-channel-message-association-message-folder-standard-flat-field-metadata.util';
@@ -43,6 +44,7 @@ const STANDARD_FLAT_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME = {
calendarEventParticipant:
buildCalendarEventParticipantStandardFlatFieldMetadatas,
calendarEvent: buildCalendarEventStandardFlatFieldMetadatas,
callRecording: buildCallRecordingStandardFlatFieldMetadatas,
company: buildCompanyStandardFlatFieldMetadatas,
dashboard: buildDashboardStandardFlatFieldMetadatas,
message: buildMessageStandardFlatFieldMetadatas,
@@ -401,6 +401,48 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
recordingPreference: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'recordingPreference',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Recording Preference`),
description: i18nLabel(
msg`Whether to record this event, applied on top of the workspace policy`,
),
icon: 'IconSettingsAutomation',
isNullable: false,
defaultValue: "'AUTO'",
options: [
{
id: '4c4761ce-ffbf-4176-be7f-5cf5257c8bff',
value: 'AUTO',
label: i18nLabel(msg`Auto`),
position: 0,
color: 'blue',
},
{
id: '1ae19135-e1a1-4a96-b866-91643622e554',
value: 'ON',
label: i18nLabel(msg`On`),
position: 1,
color: 'green',
},
{
id: '8c69a74f-2ab7-4c19-a813-eb0ea3533fd3',
value: 'OFF',
label: i18nLabel(msg`Off`),
position: 2,
color: 'gray',
},
],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
calendarChannelEventAssociations: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
@@ -447,4 +489,27 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
callRecordings: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'callRecordings',
label: i18nLabel(msg`Call Recordings`),
description: i18nLabel(msg`Call Recordings`),
icon: 'IconPhone',
isNullable: false,
isUIReadOnly: true,
targetObjectName: 'callRecording',
targetFieldName: 'calendarEvent',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -0,0 +1,464 @@
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 buildCallRecordingStandardFlatFieldMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<
CreateStandardFieldArgs<'callRecording', FieldMetadataType>,
'context'
>): Record<AllStandardObjectFieldName<'callRecording'>, 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,
isUIReadOnly: true,
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,
isUIReadOnly: true,
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,
isUIReadOnly: true,
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,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
title: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'title',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Title`),
description: i18nLabel(msg`Meeting title from the calendar event`),
icon: 'IconNotes',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
status: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'status',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Status`),
description: i18nLabel(msg`Recording lifecycle status`),
icon: 'IconProgress',
isNullable: false,
isUIReadOnly: true,
defaultValue: "'SCHEDULED'",
options: [
{
id: '7fa515ba-e3cb-48f7-914f-e1f664d5d920',
value: 'SCHEDULED',
label: i18nLabel(msg`Scheduled`),
position: 0,
color: 'sky',
},
{
id: '96844ba3-364b-4975-8abc-886cca92ec99',
value: 'JOINING',
label: i18nLabel(msg`Joining`),
position: 1,
color: 'blue',
},
{
id: 'eccdad8b-8424-48ba-ad7f-f38517fa83fc',
value: 'RECORDING',
label: i18nLabel(msg`Recording`),
position: 2,
color: 'red',
},
{
id: 'c8222203-5b44-4ac6-8142-0a7eb2074d7b',
value: 'PROCESSING',
label: i18nLabel(msg`Processing`),
position: 3,
color: 'orange',
},
{
id: 'd17faf71-af3c-4260-9021-2ffaaa5648c4',
value: 'COMPLETED',
label: i18nLabel(msg`Completed`),
position: 4,
color: 'green',
},
{
id: '4800777e-54a8-4464-9c01-07d6eefd04da',
value: 'FAILED_UNKNOWN',
label: i18nLabel(msg`Failed`),
position: 5,
color: 'gray',
},
],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
applicationId: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'applicationId',
type: FieldMetadataType.UUID,
label: i18nLabel(msg`Application ID`),
description: i18nLabel(
msg`Installed source app that manages or ingested this recording`,
),
icon: 'IconApps',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
externalBotId: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'externalBotId',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`External Bot ID`),
description: i18nLabel(
msg`Source app bot/session id, when the source supports bots`,
),
icon: 'IconRobot',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
externalRecordingId: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'externalRecordingId',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`External Recording ID`),
description: i18nLabel(msg`Source app recording id, when present`),
icon: 'IconId',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
startedAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'startedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Started At`),
description: i18nLabel(msg`Actual recording start`),
icon: 'IconCalendarClock',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
endedAt: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'endedAt',
type: FieldMetadataType.DATE_TIME,
label: i18nLabel(msg`Ended At`),
description: i18nLabel(msg`Actual recording end`),
icon: 'IconCalendarClock',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
video: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'video',
type: FieldMetadataType.FILES,
label: i18nLabel(msg`Video`),
description: i18nLabel(msg`Video recording`),
icon: 'IconVideo',
isNullable: true,
isUIReadOnly: true,
settings: {
maxNumberOfValues: 1,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
audio: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'audio',
type: FieldMetadataType.FILES,
label: i18nLabel(msg`Audio`),
description: i18nLabel(msg`Audio-only recording`),
icon: 'IconHeadphones',
isNullable: true,
isUIReadOnly: true,
settings: {
maxNumberOfValues: 1,
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
transcript: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'transcript',
type: FieldMetadataType.RAW_JSON,
label: i18nLabel(msg`Transcript`),
description: i18nLabel(msg`Normalized diarized transcript`),
icon: 'IconFileText',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
summary: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'summary',
type: FieldMetadataType.RICH_TEXT,
label: i18nLabel(msg`Summary`),
description: i18nLabel(msg`Recording summary`),
icon: 'IconFileText',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
calendarEvent: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'calendarEvent',
label: i18nLabel(msg`Calendar Event`),
description: i18nLabel(msg`Calendar Event`),
icon: 'IconCalendar',
isNullable: true,
isUIReadOnly: true,
targetObjectName: 'calendarEvent',
targetFieldName: 'callRecordings',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'calendarEventId',
},
},
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,
isUIReadOnly: true,
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,
isUIReadOnly: true,
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`Call recording record position`),
icon: 'IconHierarchy2',
isSystem: true,
isNullable: false,
defaultValue: 0,
},
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: 'title', type: FieldMetadataType.TEXT },
]),
},
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -7,6 +7,7 @@ import { buildAttachmentStandardFlatIndexMetadatas } from 'src/engine/workspace-
import { buildBlocklistStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-blocklist-standard-flat-index-metadata.util';
import { buildCalendarChannelEventAssociationStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-calendar-channel-event-association-standard-flat-index-metadata.util';
import { buildCalendarEventParticipantStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-calendar-event-participant-standard-flat-index-metadata.util';
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 { buildMessageChannelMessageAssociationMessageFolderStandardFlatIndexMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/index/compute-message-channel-message-association-message-folder-standard-flat-index-metadata.util';
@@ -38,6 +39,7 @@ const STANDARD_FLAT_INDEX_METADATA_BUILDERS_BY_OBJECT_NAME = {
buildCalendarChannelEventAssociationStandardFlatIndexMetadatas,
calendarEventParticipant:
buildCalendarEventParticipantStandardFlatIndexMetadatas,
callRecording: buildCallRecordingStandardFlatIndexMetadatas,
company: buildCompanyStandardFlatIndexMetadatas,
dashboard: buildDashboardStandardFlatIndexMetadatas,
message: buildMessageStandardFlatIndexMetadatas,
@@ -0,0 +1,31 @@
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 buildCallRecordingStandardFlatIndexMetadatas = ({
now,
objectName,
workspaceId,
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
}: Omit<CreateStandardIndexArgs<'callRecording'>, 'context'>): Record<
AllStandardObjectIndexName<'callRecording'>,
FlatIndexMetadata
> => ({
calendarEventIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'calendarEventIdIndex',
relatedFieldNames: ['calendarEvent'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
});
@@ -154,6 +154,36 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
twentyStandardApplicationId,
now,
}),
callRecording: ({
now,
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
dependencyFlatEntityMaps,
}: Omit<
CreateStandardObjectArgs<'callRecording'>,
'context' | 'objectName'
>) =>
createStandardObjectFlatMetadata({
objectName: 'callRecording',
dependencyFlatEntityMaps,
context: {
universalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
nameSingular: 'callRecording',
namePlural: 'callRecordings',
labelSingular: i18nLabel(msg`Call Recording`),
labelPlural: i18nLabel(msg`Call Recordings`),
description: i18nLabel(msg`A recording of a meeting`),
icon: 'IconVideo',
isSystem: true,
isAuditLogged: false,
labelIdentifierFieldMetadataName: 'title',
},
workspaceId,
standardObjectMetadataRelatedEntityIds,
twentyStandardApplicationId,
now,
}),
company: ({
now,
workspaceId,
@@ -1,6 +1,7 @@
export { STANDARD_BLOCKLIST_PAGE_LAYOUT_CONFIG } from './standard-blocklist-page-layout.config';
export { STANDARD_CALENDAR_CHANNEL_EVENT_ASSOCIATION_PAGE_LAYOUT_CONFIG } from './standard-calendar-channel-event-association-page-layout.config';
export { STANDARD_CALENDAR_EVENT_PARTICIPANT_PAGE_LAYOUT_CONFIG } from './standard-calendar-event-participant-page-layout.config';
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_CHANNEL_MESSAGE_ASSOCIATION_MESSAGE_FOLDER_PAGE_LAYOUT_CONFIG } from './standard-message-channel-message-association-message-folder-page-layout.config';
@@ -0,0 +1,56 @@
import {
STANDARD_OBJECTS,
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-shared/metadata';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
TAB_PROPS,
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 CALL_RECORDING_PAGE_TABS = {
home: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs
.home.universalIdentifier,
...TAB_PROPS.home,
widgets: {
fields: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage
.tabs.home.widgets.fields.universalIdentifier,
...WIDGET_PROPS.fields,
},
},
},
timeline: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage.tabs
.timeline.universalIdentifier,
...TAB_PROPS.timeline,
widgets: {
timeline: {
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage
.tabs.timeline.widgets.timeline.universalIdentifier,
...WIDGET_PROPS.timeline,
},
},
},
} as const satisfies Record<string, StandardPageLayoutTabConfig>;
export const STANDARD_CALL_RECORDING_PAGE_LAYOUT_CONFIG = {
name: 'Default Call Recording Layout',
type: PageLayoutType.RECORD_PAGE,
objectUniversalIdentifier: STANDARD_OBJECTS.callRecording.universalIdentifier,
universalIdentifier:
STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS.callRecordingRecordPage
.universalIdentifier,
defaultTabUniversalIdentifier: null,
tabs: CALL_RECORDING_PAGE_TABS,
} as const satisfies StandardPageLayoutConfig;
@@ -69,6 +69,7 @@ const RECORD_PAGE_FIELDS_VIEW_NAME_BY_OBJECT: Partial<
calendarChannelEventAssociation:
'calendarChannelEventAssociationRecordPageFields',
calendarEventParticipant: 'calendarEventParticipantRecordPageFields',
callRecording: 'callRecordingRecordPageFields',
company: 'companyRecordPageFields',
messageChannelMessageAssociation:
'messageChannelMessageAssociationRecordPageFields',
@@ -6,6 +6,7 @@ import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-
import { computeStandardBlocklistViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-blocklist-view-field-groups.util';
import { computeStandardCalendarChannelEventAssociationViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-calendar-channel-event-association-view-field-groups.util';
import { computeStandardCalendarEventParticipantViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-calendar-event-participant-view-field-groups.util';
import { computeStandardCallRecordingViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-call-recording-view-field-groups.util';
import { computeStandardCompanyViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-company-view-field-groups.util';
import { computeStandardMessageChannelMessageAssociationViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-message-channel-message-association-view-field-groups.util';
import { computeStandardMessageChannelMessageAssociationMessageFolderViewFieldGroups } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/compute-standard-message-channel-message-association-message-folder-view-field-groups.util';
@@ -29,6 +30,7 @@ const STANDARD_FLAT_VIEW_FIELD_GROUP_METADATA_BUILDERS_BY_OBJECT_NAME = {
computeStandardCalendarChannelEventAssociationViewFieldGroups,
calendarEventParticipant:
computeStandardCalendarEventParticipantViewFieldGroups,
callRecording: computeStandardCallRecordingViewFieldGroups,
company: computeStandardCompanyViewFieldGroups,
messageChannelMessageAssociation:
computeStandardMessageChannelMessageAssociationViewFieldGroups,
@@ -0,0 +1,24 @@
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
import {
createStandardViewFieldGroupFlatMetadata,
type CreateStandardViewFieldGroupArgs,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/create-standard-view-field-group-flat-metadata.util';
export const computeStandardCallRecordingViewFieldGroups = (
args: Omit<CreateStandardViewFieldGroupArgs<'callRecording'>, 'context'>,
): Record<string, FlatViewFieldGroup> => {
return {
callRecordingRecordPageFieldsGeneral:
createStandardViewFieldGroupFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldGroupName: 'general',
name: 'General',
position: 0,
isVisible: true,
},
}),
};
};
@@ -8,6 +8,7 @@ import { computeStandardBlocklistViewFields } from 'src/engine/workspace-manager
import { computeStandardCalendarChannelEventAssociationViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-calendar-channel-event-association-view-fields.util';
import { computeStandardCalendarEventParticipantViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-calendar-event-participant-view-fields.util';
import { computeStandardCalendarEventViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-calendar-event-view-fields.util';
import { computeStandardCallRecordingViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-call-recording-view-fields.util';
import { computeStandardCompanyViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-company-view-fields.util';
import { computeStandardDashboardViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-dashboard-view-fields.util';
import { computeStandardMessageChannelMessageAssociationMessageFolderViewFields } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/compute-standard-message-channel-message-association-message-folder-view-fields.util';
@@ -40,6 +41,7 @@ const STANDARD_FLAT_VIEW_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME = {
computeStandardCalendarChannelEventAssociationViewFields,
calendarEvent: computeStandardCalendarEventViewFields,
calendarEventParticipant: computeStandardCalendarEventParticipantViewFields,
callRecording: computeStandardCallRecordingViewFields,
company: computeStandardCompanyViewFields,
dashboard: computeStandardDashboardViewFields,
message: computeStandardMessageViewFields,
@@ -0,0 +1,156 @@
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import {
createStandardViewFieldFlatMetadata,
type CreateStandardViewFieldArgs,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/create-standard-view-field-flat-metadata.util';
export const computeStandardCallRecordingViewFields = (
args: Omit<CreateStandardViewFieldArgs<'callRecording'>, 'context'>,
): Record<string, FlatViewField> => {
return {
// title is the label identifier; it must hold the lowest position in non-widget views.
allCallRecordingsTitle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'allCallRecordings',
viewFieldName: 'title',
fieldName: 'title',
position: 0,
isVisible: true,
size: 200,
},
}),
allCallRecordingsStatus: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'allCallRecordings',
viewFieldName: 'status',
fieldName: 'status',
position: 1,
isVisible: true,
size: 150,
},
}),
allCallRecordingsStartedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'allCallRecordings',
viewFieldName: 'startedAt',
fieldName: 'startedAt',
position: 2,
isVisible: true,
size: 150,
},
}),
callRecordingRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'title',
fieldName: 'title',
position: 0,
isVisible: true,
size: 200,
viewFieldGroupName: 'general',
},
}),
callRecordingRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'status',
fieldName: 'status',
position: 1,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
callRecordingRecordPageFieldsStartedAt: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'startedAt',
fieldName: 'startedAt',
position: 2,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
},
),
callRecordingRecordPageFieldsEndedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'endedAt',
fieldName: 'endedAt',
position: 3,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
callRecordingRecordPageFieldsVideo: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'video',
fieldName: 'video',
position: 4,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
callRecordingRecordPageFieldsAudio: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'audio',
fieldName: 'audio',
position: 5,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
callRecordingRecordPageFieldsTranscript:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'transcript',
fieldName: 'transcript',
position: 6,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
callRecordingRecordPageFieldsSummary: createStandardViewFieldFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
viewFieldName: 'summary',
fieldName: 'summary',
position: 7,
isVisible: true,
size: 200,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -8,6 +8,7 @@ import { computeStandardBlocklistViews } from 'src/engine/workspace-manager/twen
import { computeStandardCalendarChannelEventAssociationViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-calendar-channel-event-association-views.util';
import { computeStandardCalendarEventParticipantViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-calendar-event-participant-views.util';
import { computeStandardCalendarEventViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-calendar-event-views.util';
import { computeStandardCallRecordingViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-call-recording-views.util';
import { computeStandardCompanyViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-company-views.util';
import { computeStandardDashboardViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-dashboard-views.util';
import { computeStandardMessageChannelMessageAssociationMessageFolderViews } from 'src/engine/workspace-manager/twenty-standard-application/utils/view/compute-standard-message-channel-message-association-message-folder-views.util';
@@ -40,6 +41,7 @@ const STANDARD_FLAT_VIEW_METADATA_BUILDERS_BY_OBJECT_NAME = {
computeStandardCalendarChannelEventAssociationViews,
calendarEvent: computeStandardCalendarEventViews,
calendarEventParticipant: computeStandardCalendarEventParticipantViews,
callRecording: computeStandardCallRecordingViews,
company: computeStandardCompanyViews,
dashboard: computeStandardDashboardViews,
message: computeStandardMessageViews,
@@ -0,0 +1,38 @@
import { ViewKey, ViewType } from 'twenty-shared/types';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import {
createStandardViewFlatMetadata,
type CreateStandardViewArgs,
} from 'src/engine/workspace-manager/twenty-standard-application/utils/view/create-standard-view-flat-metadata.util';
export const computeStandardCallRecordingViews = (
args: Omit<CreateStandardViewArgs<'callRecording'>, 'context'>,
): Record<string, FlatView> => {
return {
allCallRecordings: createStandardViewFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'allCallRecordings',
name: 'All {objectLabelPlural}',
type: ViewType.TABLE,
key: ViewKey.INDEX,
position: 0,
icon: 'IconList',
},
}),
callRecordingRecordPageFields: createStandardViewFlatMetadata({
...args,
objectName: 'callRecording',
context: {
viewName: 'callRecordingRecordPageFields',
name: 'Call Recording Record Page Fields',
type: ViewType.FIELDS_WIDGET,
key: null,
position: 0,
icon: 'IconList',
},
}),
};
};
@@ -8,6 +8,7 @@ export const STANDARD_OBJECT_ICONS = {
calendarChannelEventAssociation: 'IconCalendar',
calendarEventParticipant: 'IconCalendar',
calendarEvent: 'IconCalendar',
callRecording: 'IconVideo',
comment: 'IconMessageCircle',
company: 'IconBuildingSkyscraper',
dashboard: 'IconLayoutDashboard',
@@ -37,6 +37,7 @@ const createMockCalendarEvent = (
updatedAt: '2024-03-20T09:00:00Z',
iCalUid: '',
conferenceSolution: '',
recordingPreference: 'AUTO',
calendarChannelEventAssociations: [],
calendarEventParticipants: [],
});
@@ -25,10 +25,14 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
iCalUid: string | null;
conferenceSolution: string | null;
conferenceLink: LinksMetadata;
recordingPreference: string;
calendarChannelEventAssociations: EntityRelation<
CalendarChannelEventAssociationWorkspaceEntity[]
>;
calendarEventParticipants: EntityRelation<
CalendarEventParticipantWorkspaceEntity[]
>;
// callRecordings reverse relation intentionally omitted from the TypeScript workspace
// entity. It exists in standard metadata, but declaring a to-many relation here expands
// recursive nested insert types and tips Person past TS's instantiation-depth limit.
}
@@ -0,0 +1,22 @@
import { type RichTextMetadata } from 'twenty-shared/types';
import { type FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
export class CallRecordingWorkspaceEntity extends BaseWorkspaceEntity {
title: string | null;
status: string;
applicationId: string | null;
externalBotId: string | null;
externalRecordingId: string | null;
startedAt: string | null;
endedAt: string | null;
video: FileOutput[] | null;
audio: FileOutput[] | null;
transcript: Record<string, unknown> | null;
summary: RichTextMetadata | null;
calendarEvent: EntityRelation<CalendarEventWorkspaceEntity> | null;
calendarEventId: string | null;
}
@@ -480,6 +480,12 @@ export const STANDARD_OBJECTS = {
calendarEventParticipants: {
universalIdentifier: '20202020-e07e-4ccb-88f5-6f3d00458eec',
},
recordingPreference: {
universalIdentifier: '1d231e7e-9bbe-410b-8007-ea7678a83e58',
},
callRecordings: {
universalIdentifier: '48d6d151-18e2-4111-b405-d85fb9d860d8',
},
createdBy: {
universalIdentifier: '664a9500-2641-4caa-8d95-069807bb2eb4',
},
@@ -526,6 +532,126 @@ export const STANDARD_OBJECTS = {
},
},
},
callRecording: {
universalIdentifier: 'ce19efb9-710f-45b2-b141-473abbeea60b',
fields: {
id: {
universalIdentifier: '9767180b-76c2-47d5-b7b9-5dc60f14391d',
},
createdAt: {
universalIdentifier: 'd1adb2a9-9da3-4656-93b7-db79780b77eb',
},
updatedAt: {
universalIdentifier: '5db0a477-5715-4ffd-bdbe-854dfaccfe22',
},
deletedAt: {
universalIdentifier: 'cf22a652-390a-4f55-b3a0-615c8fd8c7ed',
},
title: {
universalIdentifier: '4cff8863-a1d1-45fd-a370-4eb6aa1f2a5b',
},
status: {
universalIdentifier: '3e617680-d93e-4309-a54f-90f69528bfd7',
},
applicationId: {
universalIdentifier: '24ec1239-1240-42cb-8a2d-302632378e09',
},
externalBotId: {
universalIdentifier: '0a2da128-9bcc-488b-bc31-65318c41bdf9',
},
externalRecordingId: {
universalIdentifier: '6d17fb71-324b-4625-a5be-b3580607e2c7',
},
startedAt: {
universalIdentifier: '6c56c23f-1987-410a-860a-df3b2b3f9a33',
},
endedAt: {
universalIdentifier: '7a38a9cf-8424-4d6e-b80a-6883d3c662ef',
},
video: {
universalIdentifier: 'bb9523d3-457e-4f4b-8c79-27a77afb87da',
},
audio: {
universalIdentifier: '2eafc2d0-8fec-430c-a939-65ca5fbc0f08',
},
transcript: {
universalIdentifier: '27b86d68-57d1-4607-aca0-191896b1ad43',
},
summary: {
universalIdentifier: 'adb0f472-756b-4d3f-b21e-ea32bf73a5e4',
},
calendarEvent: {
universalIdentifier: '49e64b28-bd98-4775-80ea-4781bdd45e35',
},
createdBy: {
universalIdentifier: 'cb84ce87-ccd4-4901-9b9e-25e3cbfce3ba',
},
updatedBy: {
universalIdentifier: '02309262-b6dc-4cbd-936b-d851a7deec79',
},
position: {
universalIdentifier: 'eeae3145-3696-41ae-b0ed-1370ad35b33c',
},
searchVector: {
universalIdentifier: 'd6fd4ad0-4aec-4969-bdb5-945fda0fcf45',
},
},
indexes: {
calendarEventIdIndex: {
universalIdentifier: '8be3cc47-9352-4a1b-ad19-bb186bc0865d',
},
},
views: {
allCallRecordings: {
universalIdentifier: 'c395b55e-88f0-4d5b-a1fb-0d38b50e0b19',
viewFields: {
status: {
universalIdentifier: '6c4a81a2-d9c1-4f82-984c-f97e083ca710',
},
title: {
universalIdentifier: 'b1d5051b-071d-4514-93cf-704724cdc8f6',
},
startedAt: {
universalIdentifier: '3b96351f-66ed-4fa6-acb6-698647573af7',
},
},
},
callRecordingRecordPageFields: {
universalIdentifier: '99fa8b47-3b11-4f9b-8fbc-e67a9e1da682',
viewFieldGroups: {
general: {
universalIdentifier: '068426eb-dd20-49b0-ae9c-68727f3be2fb',
},
},
viewFields: {
title: {
universalIdentifier: '6308d574-8579-4cf2-a020-c208df97cf3e',
},
status: {
universalIdentifier: '93483569-fcd2-46cf-b576-9f0318ad2b3b',
},
startedAt: {
universalIdentifier: '3fd00fbb-c153-45e3-b6e6-43d18d34052a',
},
endedAt: {
universalIdentifier: 'ba8c8d41-c112-4173-b927-5b5c5a5c047b',
},
video: {
universalIdentifier: 'acc54ade-cd26-4be2-9391-a42715ad1523',
},
audio: {
universalIdentifier: '9445a547-1d1e-4da3-916b-2c2269c951c9',
},
transcript: {
universalIdentifier: '782c97f6-e6b1-472b-8992-bbb60d25791b',
},
summary: {
universalIdentifier: 'a0ace064-cc72-4631-ade3-07cdded86b0e',
},
},
},
},
},
company: {
universalIdentifier: '20202020-b374-4779-a561-80086cb2e17f',
fields: {
@@ -444,6 +444,27 @@ export const STANDARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIERS = {
},
},
},
callRecordingRecordPage: {
universalIdentifier: 'fa475fb3-3fe4-4ab8-8320-495eba5b2e58',
tabs: {
home: {
universalIdentifier: 'f61be659-f6f1-4f1f-b3f9-113370740f1b',
widgets: {
fields: {
universalIdentifier: 'e490d2dd-5b69-4518-b867-9dde815f9cb6',
},
},
},
timeline: {
universalIdentifier: '9fb360fa-bc10-430c-8425-25d4e9da4386',
widgets: {
timeline: {
universalIdentifier: '9347305a-73de-4651-a116-c67b2706beda',
},
},
},
},
},
messageChannelMessageAssociationRecordPage: {
universalIdentifier: '20202020-a111-4011-8011-a5c4a5110001',
tabs: {
@@ -8,5 +8,6 @@ export enum FeatureFlagKey {
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED',
IS_WORKFLOW_RUN_STEP_LOGS_ENABLED = 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED',
}