[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:
@@ -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',
|
||||
|
||||
+2
@@ -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 {}
|
||||
|
||||
+425
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+241
@@ -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);
|
||||
});
|
||||
});
|
||||
+467
@@ -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']);
|
||||
});
|
||||
});
|
||||
+177
@@ -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');
|
||||
});
|
||||
});
|
||||
+98
@@ -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 };
|
||||
};
|
||||
+204
@@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
+62
@@ -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;
|
||||
};
|
||||
+48
@@ -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');
|
||||
});
|
||||
});
|
||||
+34
-1
@@ -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,
|
||||
|
||||
+1
@@ -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
@@ -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,
|
||||
|
||||
+811
-687
File diff suppressed because it is too large
Load Diff
+231
-210
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+139
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
@@ -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,
|
||||
|
||||
+65
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
+464
@@ -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,
|
||||
}),
|
||||
});
|
||||
+2
@@ -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,
|
||||
|
||||
+31
@@ -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,
|
||||
}),
|
||||
});
|
||||
+30
@@ -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
@@ -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';
|
||||
|
||||
+56
@@ -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;
|
||||
+1
@@ -69,6 +69,7 @@ const RECORD_PAGE_FIELDS_VIEW_NAME_BY_OBJECT: Partial<
|
||||
calendarChannelEventAssociation:
|
||||
'calendarChannelEventAssociationRecordPageFields',
|
||||
calendarEventParticipant: 'calendarEventParticipantRecordPageFields',
|
||||
callRecording: 'callRecordingRecordPageFields',
|
||||
company: 'companyRecordPageFields',
|
||||
messageChannelMessageAssociation:
|
||||
'messageChannelMessageAssociationRecordPageFields',
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+24
@@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
+2
@@ -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,
|
||||
|
||||
+156
@@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
+2
@@ -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,
|
||||
|
||||
+38
@@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
+1
@@ -8,6 +8,7 @@ export const STANDARD_OBJECT_ICONS = {
|
||||
calendarChannelEventAssociation: 'IconCalendar',
|
||||
calendarEventParticipant: 'IconCalendar',
|
||||
calendarEvent: 'IconCalendar',
|
||||
callRecording: 'IconVideo',
|
||||
comment: 'IconMessageCircle',
|
||||
company: 'IconBuildingSkyscraper',
|
||||
dashboard: 'IconLayoutDashboard',
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ const createMockCalendarEvent = (
|
||||
updatedAt: '2024-03-20T09:00:00Z',
|
||||
iCalUid: '',
|
||||
conferenceSolution: '',
|
||||
recordingPreference: 'AUTO',
|
||||
calendarChannelEventAssociations: [],
|
||||
calendarEventParticipants: [],
|
||||
});
|
||||
|
||||
+4
@@ -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.
|
||||
}
|
||||
|
||||
+22
@@ -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: {
|
||||
|
||||
+21
@@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user