Add Create related records to Record standard actions (#13095)

#12924 Add Create related records to Record standard actions
- add the "Create related records" option to the standard Record actions
in the command menu.
- apply to one-to-many relations.
- command should open a side panel with an empty record for the selected
object.

<img width="518" alt="Screenshot"
src="https://github.com/user-attachments/assets/0388aaf9-b974-4ae1-85bf-2966d89cbbec"
/>

---------

Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
Jeremy Lim
2025-07-18 21:17:29 +08:00
committed by GitHub
parent 6e5487ed76
commit 56812cce53
21 changed files with 547 additions and 40 deletions

View File

@ -0,0 +1,120 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { isDefined } from 'twenty-shared/utils';
interface CreateRelatedRecordActionProps {
targetFieldMetadataItemRelation: FieldMetadataItemRelation;
}
export const CreateRelatedRecordAction = ({
targetFieldMetadataItemRelation,
}: CreateRelatedRecordActionProps) => {
const sourceRecordId = useSelectedRecordIdOrThrow();
const { objectMetadataItem: targetObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular:
targetFieldMetadataItemRelation.targetObjectMetadata.nameSingular,
});
const { objectMetadataItem: taskObjectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Task,
});
const { objectMetadataItem: noteObjectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Note,
});
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const { createOneRecord: createOneTaskTarget } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.TaskTarget,
});
const { createOneRecord: createOneNoteTarget } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.NoteTarget,
});
const { openRecordTitleCell } = useRecordTitleCell();
const targetObject =
targetObjectMetadataItem.nameSingular === CoreObjectNameSingular.TaskTarget
? taskObjectMetadataItem
: targetObjectMetadataItem.nameSingular ===
CoreObjectNameSingular.NoteTarget
? noteObjectMetadataItem
: targetObjectMetadataItem;
const { createOneRecord } = useCreateOneRecord({
objectNameSingular: targetObject.nameSingular,
});
const handleCreateRelatedRecord = async () => {
const foreignKeyFieldName =
targetFieldMetadataItemRelation.targetFieldMetadata.name;
const foreignKeyIdFieldName =
getForeignKeyNameFromRelationFieldName(foreignKeyFieldName);
let createdRecord: ObjectRecord;
switch (targetObjectMetadataItem.nameSingular) {
case CoreObjectNameSingular.TaskTarget: {
createdRecord = await createOneRecord({});
await createOneTaskTarget({
taskId: createdRecord.id,
[foreignKeyIdFieldName]: sourceRecordId,
});
break;
}
case CoreObjectNameSingular.NoteTarget: {
createdRecord = await createOneRecord({});
await createOneNoteTarget({
noteId: createdRecord.id,
[foreignKeyIdFieldName]: sourceRecordId,
});
break;
}
default:
createdRecord = await createOneRecord({
[foreignKeyIdFieldName]: sourceRecordId,
});
break;
}
openRecordInCommandMenu({
recordId: createdRecord.id,
objectNameSingular: targetObject.nameSingular,
isNewRecord: true,
});
const labelIdentifierFieldMetadataItem =
getLabelIdentifierFieldMetadataItem(targetObject);
if (isDefined(labelIdentifierFieldMetadataItem)) {
openRecordTitleCell({
recordId: createdRecord.id,
fieldName: labelIdentifierFieldMetadataItem.name,
containerType: RecordTitleCellContainerType.ShowPage,
});
}
};
return (
<Action
onClick={handleCreateRelatedRecord}
closeSidePanelOnCommandMenuListActionExecution={false}
/>
);
};

View File

@ -0,0 +1,253 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useRelatedRecordActions } from '../useRelatedRecordActions';
jest.mock('@/object-metadata/hooks/useObjectMetadataItems', () => ({
useObjectMetadataItems: () => ({
objectMetadataItems: [
{
id: 'person-id',
nameSingular: CoreObjectNameSingular.Person,
namePlural: 'People',
labelSingular: 'Person',
},
{
id: 'company-id',
nameSingular: CoreObjectNameSingular.Company,
namePlural: 'Companies',
labelSingular: 'Company',
},
],
}),
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
describe('useRelatedRecordActions', () => {
const mockGetIcon = jest.fn();
beforeEach(() => {
mockGetIcon.mockClear();
});
it('should return empty object when objectMetadataItem has no fields', () => {
const objectMetadataItem = {
fields: [],
} as unknown as ObjectMetadataItem;
const { result } = renderHook(
() =>
useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon: mockGetIcon,
startPosition: 18,
}),
{ wrapper: Wrapper },
);
expect(result.current).toEqual({});
});
it('should return empty object when objectMetadataItem is undefined', () => {
const objectMetadataItem = undefined as unknown as ObjectMetadataItem;
const { result } = renderHook(
() =>
useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon: mockGetIcon,
startPosition: 18,
}),
{ wrapper: Wrapper },
);
expect(result.current).toEqual({});
});
it('should generate actions for one-to-many relations', () => {
const objectMetadataItem = {
fields: [
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Person,
namePlural: 'People',
},
},
label: 'person',
isSystem: false,
},
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Company,
namePlural: 'Companies',
},
},
label: 'company',
isSystem: false,
},
],
} as unknown as ObjectMetadataItem;
const { result } = renderHook(
() =>
useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon: mockGetIcon,
startPosition: 18,
}),
{ wrapper: Wrapper },
);
expect(Object.keys(result.current)).toHaveLength(2);
expect(result.current['create-related-person']).toBeDefined();
expect(result.current['create-related-company']).toBeDefined();
});
it('should filter out non-one-to-many relations', () => {
const objectMetadataItem = {
fields: [
{
type: 'RELATION',
relation: {
type: 'MANY_TO_ONE',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Person,
namePlural: 'People',
},
},
label: 'person',
isSystem: false,
},
{
type: 'TEXT',
},
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Company,
namePlural: 'Companies',
},
},
label: 'company',
isSystem: false,
},
],
} as unknown as ObjectMetadataItem;
const { result } = renderHook(
() =>
useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon: mockGetIcon,
startPosition: 18,
}),
{ wrapper: Wrapper },
);
expect(Object.keys(result.current)).toHaveLength(1);
expect(result.current['create-related-company']).toBeDefined();
expect(result.current['create-related-person']).toBeUndefined();
});
it('should assign correct positions to each action', () => {
const objectMetadataItem = {
fields: [
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Person,
namePlural: 'People',
},
},
label: 'person',
isSystem: false,
},
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Company,
namePlural: 'Companies',
},
},
label: 'company',
isSystem: false,
},
],
} as unknown as ObjectMetadataItem;
const { result } = renderHook(
() =>
useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon: mockGetIcon,
startPosition: 18,
}),
{ wrapper: Wrapper },
);
expect(result.current['create-related-person'].position).toBe(18);
expect(result.current['create-related-company'].position).toBe(19);
});
it('should filter out system fields', () => {
const objectMetadataItem = {
fields: [
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Person,
namePlural: 'People',
},
},
label: 'person',
isSystem: true,
},
{
type: 'RELATION',
relation: {
type: 'ONE_TO_MANY',
targetObjectMetadata: {
nameSingular: CoreObjectNameSingular.Company,
namePlural: 'Companies',
},
},
label: 'company',
isSystem: false,
},
],
} as unknown as ObjectMetadataItem;
const { result } = renderHook(
() =>
useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon: mockGetIcon,
startPosition: 18,
}),
{ wrapper: Wrapper },
);
expect(Object.keys(result.current)).toHaveLength(1);
expect(result.current['create-related-company']).toBeDefined();
expect(result.current['create-related-person']).toBeUndefined();
});
});

View File

@ -0,0 +1,107 @@
import { CreateRelatedRecordAction } from '@/action-menu/actions/record-actions/single-record/components/CreateRelatedRecordAction';
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { msg } from '@lingui/core/macro';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconComponent, IconPlus } from 'twenty-ui/display';
interface GenerateRelatedRecordActionsParams {
sourceObjectMetadataItem?: ObjectMetadataItem;
getIcon: (iconKey: string) => IconComponent;
startPosition: number;
}
export const useRelatedRecordActions = ({
sourceObjectMetadataItem,
getIcon,
startPosition,
}: GenerateRelatedRecordActionsParams): Record<string, ActionConfig> => {
const relatedActions: Record<string, ActionConfig> = {};
const { objectMetadataItems } = useObjectMetadataItems();
if (!sourceObjectMetadataItem?.fields) {
return relatedActions;
}
const oneToManyFields = sourceObjectMetadataItem.fields.filter(
(field) =>
field.type === 'RELATION' &&
field.relation?.type === 'ONE_TO_MANY' &&
!field.isSystem,
);
let currentPosition = startPosition;
for (const field of oneToManyFields) {
if (!isDefined(field.relation)) {
throw new Error(`Field relation is undefined for field: ${field.id}`);
}
const targetObjectName = field.relation.targetObjectMetadata.nameSingular;
const targetObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === targetObjectName,
);
if (!isDefined(targetObjectMetadataItem)) {
throw new Error(
`Target object metadata item is undefined for field: ${field.id}`,
);
}
const targetObjectNameSingular = targetObjectMetadataItem.nameSingular;
const targetObjectLabelSingular =
targetObjectNameSingular === CoreObjectNameSingular.TaskTarget
? 'task'
: targetObjectNameSingular === CoreObjectNameSingular.NoteTarget
? 'note'
: targetObjectMetadataItem.labelSingular.toLowerCase();
const actionKey = `create-related-${targetObjectLabelSingular}`;
relatedActions[actionKey] = {
type: ActionType.Standard,
scope: ActionScope.CreateRelatedRecord,
key: actionKey,
label: msg`Create ${targetObjectLabelSingular}`,
shortLabel: msg`Create ${targetObjectLabelSingular}`,
position: currentPosition,
Icon: field.icon ? getIcon(field.icon) : IconPlus,
accent: 'default',
isPinned: false,
shouldBeRegistered: ({
selectedRecord,
objectPermissions,
getTargetObjectWritePermission,
}) =>
isDefined(selectedRecord) &&
!selectedRecord.isRemote &&
objectPermissions.canUpdateObjectRecords &&
getTargetObjectWritePermission(
targetObjectNameSingular === CoreObjectNameSingular.TaskTarget
? CoreObjectNameSingular.Task
: targetObjectNameSingular === CoreObjectNameSingular.NoteTarget
? CoreObjectNameSingular.Note
: targetObjectNameSingular,
),
availableOn: [
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
component: React.createElement(CreateRelatedRecordAction, {
targetFieldMetadataItemRelation: field.relation,
}),
};
currentPosition++;
}
return relatedActions;
};

View File

@ -1,5 +1,6 @@
export enum ActionScope {
Global = 'Global',
RecordSelection = 'RecordSelection',
CreateRelatedRecord = 'CreateRelatedRecord',
Object = 'Object',
}

View File

@ -22,4 +22,7 @@ export type ShouldBeRegisteredFunctionParams = {
getTargetObjectReadPermission: (
objectMetadataItemNameSingular: string,
) => boolean;
getTargetObjectWritePermission: (
objectMetadataItemNameSingular: string,
) => boolean;
};

View File

@ -26,7 +26,8 @@ export const getActionConfig = ({
case CoreObjectNameSingular.WorkflowRun: {
return WORKFLOW_RUNS_ACTIONS_CONFIG;
}
default:
default: {
return DEFAULT_RECORD_ACTIONS_CONFIG;
}
}
};

View File

@ -1,4 +1,5 @@
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
import { useRelatedRecordActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRelatedRecordActions';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
@ -7,12 +8,15 @@ import { contextStoreCurrentViewTypeComponentState } from '@/context-store/state
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
export const useRegisteredActions = (
shouldBeRegisteredParams: ShouldBeRegisteredFunctionParams,
) => {
const { objectMetadataItem } = shouldBeRegisteredParams;
const { getIcon } = useIcons();
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
);
@ -30,10 +34,17 @@ export const useRegisteredActions = (
objectMetadataItem,
});
const relatedRecordActionConfig = useRelatedRecordActions({
sourceObjectMetadataItem: objectMetadataItem,
getIcon,
startPosition: Object.keys(recordActionConfig).length + 1,
});
const recordAgnosticActionConfig = useRecordAgnosticActions();
const actionsConfig = {
...recordActionConfig,
...relatedRecordActionConfig,
...recordAgnosticActionConfig,
};

View File

@ -86,6 +86,20 @@ export const useShouldActionBeRegisteredParams = ({
[],
);
const getObjectWritePermission = useRecoilCallback(
({ snapshot }) =>
(objectMetadataNameSingular: string) => {
return snapshot
.getLoadable(
objectPermissionsFamilySelector({
objectNameSingular: objectMetadataNameSingular,
}),
)
.getValue().canUpdate;
},
[],
);
return {
objectMetadataItem,
isFavorite,
@ -98,5 +112,6 @@ export const useShouldActionBeRegisteredParams = ({
numberOfSelectedRecords,
viewType: viewType ?? undefined,
getTargetObjectReadPermission: getObjectReadPermission,
getTargetObjectWritePermission: getObjectWritePermission,
};
};

View File

@ -5,6 +5,7 @@ import { selectorFamily } from 'recoil';
export const objectPermissionsFamilySelector = selectorFamily<
{
canRead: boolean;
canUpdate: boolean;
},
{ objectNameSingular: string }
>({
@ -32,6 +33,7 @@ export const objectPermissionsFamilySelector = selectorFamily<
return {
canRead: objectPermissions?.canReadObjectRecords ?? false,
canUpdate: objectPermissions?.canUpdateObjectRecords ?? false,
};
},
});

View File

@ -32,6 +32,7 @@ export const CommandMenu = () => {
matchingWorkflowRunGlobalActions,
matchingNavigateActions,
fallbackActions,
matchingCreateRelatedRecordActions,
} = useMatchingCommandMenuActions({
commandMenuSearch,
});
@ -56,6 +57,10 @@ export const CommandMenu = () => {
matchingWorkflowRunRecordSelectionActions,
),
},
{
heading: t`Create Related Record`,
items: matchingCreateRelatedRecordActions,
},
{
heading: currentObjectMetadataItem?.labelPlural ?? t`Object`,
items: matchingStandardActionObjectActions,

View File

@ -45,6 +45,12 @@ export const useCommandMenuActions = () => {
(action) => action.type === ActionType.Fallback,
);
const createRelatedRecordActions: ActionConfig[] = actions?.filter(
(action) =>
action.type === ActionType.Standard &&
action.scope === ActionScope.CreateRelatedRecord,
);
return {
navigateActions,
actionRecordSelectionActions,
@ -53,5 +59,6 @@ export const useCommandMenuActions = () => {
workflowRunRecordSelectionActions,
workflowRunGlobalActions,
fallbackActions,
createRelatedRecordActions,
};
};

View File

@ -19,6 +19,7 @@ export const useMatchingCommandMenuActions = ({
workflowRunRecordSelectionActions,
workflowRunGlobalActions,
fallbackActions,
createRelatedRecordActions,
} = useCommandMenuActions();
const matchingNavigateActions =
@ -40,13 +41,18 @@ export const useMatchingCommandMenuActions = ({
workflowRunGlobalActions,
);
const matchingCreateRelatedRecordActions = filterActionsWithCommandMenuSearch(
createRelatedRecordActions,
);
const noResults =
!matchingStandardActionRecordSelectionActions.length &&
!matchingWorkflowRunRecordSelectionActions.length &&
!matchingStandardActionGlobalActions.length &&
!matchingWorkflowRunGlobalActions.length &&
!matchingStandardActionObjectActions.length &&
!matchingNavigateActions.length;
!matchingNavigateActions.length &&
!matchingCreateRelatedRecordActions.length;
return {
noResults,
@ -56,6 +62,7 @@ export const useMatchingCommandMenuActions = ({
matchingStandardActionGlobalActions,
matchingWorkflowRunGlobalActions,
matchingNavigateActions,
matchingCreateRelatedRecordActions,
fallbackActions: noResults ? fallbackActions : [],
};
};

View File

@ -1,15 +0,0 @@
import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
describe('getObjectMetadataItemBySingularName', () => {
it('should work as expected', () => {
const firstObjectMetadataItem = generatedMockObjectMetadataItems[0];
const foundObjectMetadataItem = getObjectMetadataItemByNameSingular({
objectMetadataItems: generatedMockObjectMetadataItems,
objectNameSingular: firstObjectMetadataItem.nameSingular,
});
expect(foundObjectMetadataItem.id).toEqual(firstObjectMetadataItem.id);
});
});

View File

@ -1,22 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getObjectMetadataItemByNameSingular = ({
objectMetadataItems,
objectNameSingular,
}: {
objectMetadataItems: ObjectMetadataItem[];
objectNameSingular: string;
}) => {
const foundObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!foundObjectMetadataItem) {
throw new Error(
`Could not find object metadata item with singular name ${objectNameSingular}`,
);
}
return foundObjectMetadataItem;
};

View File

@ -126,6 +126,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
attachments: AttachmentWorkspaceEntity[];
@WorkspaceRelation({

View File

@ -245,6 +245,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
attachments: Relation<AttachmentWorkspaceEntity[]>;
@WorkspaceRelation({

View File

@ -108,6 +108,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
noteTargets: Relation<NoteTargetWorkspaceEntity[]>;
@WorkspaceRelation({
@ -120,6 +121,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
attachments: Relation<AttachmentWorkspaceEntity[]>;
@WorkspaceRelation({
@ -132,6 +134,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
@WorkspaceRelation({

View File

@ -201,6 +201,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
attachments: Relation<AttachmentWorkspaceEntity[]>;
@WorkspaceRelation({
@ -213,6 +214,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
@WorkspaceField({

View File

@ -196,7 +196,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceRelation({
standardId: PERSON_STANDARD_FIELD_IDS.pointOfContactForOpportunities,
type: RelationType.ONE_TO_MANY,
label: msg`Linked Opportunities`,
label: msg`Opportunities`,
description: msg`List of opportunities for which that person is the point of contact`,
icon: 'IconTargetArrow',
inverseSideTarget: () => OpportunityWorkspaceEntity,
@ -248,6 +248,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => AttachmentWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsSystem()
attachments: Relation<AttachmentWorkspaceEntity[]>;
@WorkspaceRelation({

View File

@ -147,6 +147,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
taskTargets: Relation<TaskTargetWorkspaceEntity[]>;
@WorkspaceRelation({
@ -159,6 +160,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
attachments: Relation<AttachmentWorkspaceEntity[]>;
@WorkspaceRelation({
@ -187,6 +189,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
@WorkspaceRelation({

View File

@ -279,6 +279,7 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideFieldKey: 'author',
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsSystem()
authoredAttachments: Relation<AttachmentWorkspaceEntity[]>;
@WorkspaceRelation({