Compare commits

...

3 Commits

Author SHA1 Message Date
Baptiste Devessier 371fc9dedc Merge branch 'main' into include-activity-target-relations-rpl 2026-04-14 22:48:26 +02:00
Devessier b145d4227a fix: import 2026-04-14 18:58:02 +02:00
Devessier 94446a0529 feat: include activity target relations in default fields widget config 2026-04-14 18:40:46 +02:00
11 changed files with 157 additions and 42 deletions
@@ -1,11 +1,8 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isJunctionRelationField } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationField';
import {
CoreObjectNameSingular,
type ObjectPermissions,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isActivityTargetField, isDefined } from 'twenty-shared/utils';
type ObjectPermissionsByObjectMetadataId = Record<
string,
@@ -26,15 +23,6 @@ type CategorizedRelationFields = {
boxedRelationFields: FieldMetadataItem[];
};
export const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
const isActivityTargetRelation = (
fieldMetadataItem: FieldMetadataItem,
objectNameSingular: string,
@@ -4,21 +4,20 @@ import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTa
import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { RecordChip } from '@/object-record/components/RecordChip';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus';
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRelationFromManyFieldDisplay';
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRelationFromManyFieldDisplay';
import { extractTargetRecordsFromJunction } from '@/object-record/record-field/ui/utils/junction/extractTargetRecordsFromJunction';
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { styled } from '@linaria/react';
import { isArray } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { isActivityTargetField, isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
@@ -1,5 +1,5 @@
import { useCallback, useContext } from 'react';
import { useStore } from 'jotai';
import { useCallback, useContext } from 'react';
import { v4 } from 'uuid';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
@@ -8,10 +8,8 @@ import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type CoreObjectNameSingular } from 'twenty-shared/types';
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { useUpdateJunctionRelationFromCell } from '@/object-record/record-field/ui/hooks/useUpdateJunctionRelationFromCell';
@@ -35,6 +33,7 @@ import { buildRecordLabelPayload } from '@/object-record/utils/buildRecordLabelP
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { type CoreObjectNameSingular } from 'twenty-shared/types';
import { CustomError, isDefined } from 'twenty-shared/utils';
export const RelationOneToManyFieldInput = () => {
@@ -1,9 +1,8 @@
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import {
FieldContext,
type RecordUpdateHook,
@@ -17,6 +16,7 @@ import {
type CoreObjectNameSingular,
type ObjectPermissions,
} from 'twenty-shared/types';
import { isActivityTargetField } from 'twenty-shared/utils';
type FieldsWidgetFieldItemProps = {
fieldMetadataItem: FieldMetadataItem;
@@ -1,7 +1,10 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { FieldMetadataType } from 'twenty-shared/types';
import { isFieldMetadataEligibleForFieldsWidget } from 'twenty-shared/utils';
import {
isActivityTargetField,
isFieldMetadataEligibleForFieldsWidget,
} from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
export const buildDefaultFieldsWidgetGroups = ({
@@ -24,9 +27,10 @@ export const buildDefaultFieldsWidgetGroups = ({
const standardFields = eligibleFields.filter((field) => !field.isCustom);
const customFields = eligibleFields.filter((field) => field.isCustom);
const isFieldVisible = (fieldType: FieldMetadataType) =>
fieldType !== FieldMetadataType.RELATION &&
fieldType !== FieldMetadataType.MORPH_RELATION;
const isFieldVisible = (fieldType: FieldMetadataType, fieldName: string) =>
isActivityTargetField(fieldName, objectNameSingular) ||
(fieldType !== FieldMetadataType.RELATION &&
fieldType !== FieldMetadataType.MORPH_RELATION);
const groups: FieldsWidgetGroup[] = [];
let globalIndex = 0;
@@ -40,7 +44,7 @@ export const buildDefaultFieldsWidgetGroups = ({
fields: standardFields.map((field, index) => ({
fieldMetadataItem: field,
position: index,
isVisible: isFieldVisible(field.type),
isVisible: isFieldVisible(field.type, field.name),
globalIndex: globalIndex++,
})),
});
@@ -55,7 +59,7 @@ export const buildDefaultFieldsWidgetGroups = ({
fields: customFields.map((field, index) => ({
fieldMetadataItem: field,
position: index,
isVisible: isFieldVisible(field.type),
isVisible: isFieldVisible(field.type, field.name),
globalIndex: globalIndex++,
})),
});
@@ -110,6 +110,7 @@ export class ViewService {
flatApplication: workspaceCustomFlatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
objectFlatMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
objectNameSingular: objectFlatMetadata.nameSingular,
},
);
@@ -68,6 +68,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(result.flatViewFieldGroupsToCreate).toHaveLength(1);
@@ -113,6 +114,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(result.flatViewFieldGroupsToCreate).toHaveLength(2);
@@ -155,6 +157,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(result.flatViewFieldGroupsToCreate).toHaveLength(1);
@@ -180,6 +183,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(result.flatViewFieldsToCreate).toHaveLength(1);
@@ -212,6 +216,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(result.flatViewFieldsToCreate).toHaveLength(1);
@@ -240,6 +245,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(resultWithout.flatViewFieldsToCreate).toHaveLength(1);
@@ -250,6 +256,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: idUid,
objectNameSingular: 'person',
});
expect(resultWith.flatViewFieldsToCreate).toHaveLength(1);
@@ -274,6 +281,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
result.flatViewFieldGroupsToCreate.forEach((group) => {
@@ -303,6 +311,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
// Still creates the "General" group, just with no fields
@@ -337,6 +346,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: labelUid,
objectNameSingular: 'person',
});
// Label identifier field is excluded by isFieldMetadataEligibleForFieldsWidget
@@ -379,6 +389,7 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
const viewFieldForName = result.flatViewFieldsToCreate.find(
@@ -403,4 +414,104 @@ describe('computeFieldsWidgetViewFieldsAndGroupsToCreate', () => {
expect(viewFieldForTarget!.isVisible).toBe(false);
expect(viewFieldForCustomRelation!.isVisible).toBe(false);
});
it('should make activity target RELATION fields visible by default for note objects', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
isCustom: false,
}),
makeFieldMetadata({
name: 'noteTargets',
type: FieldMetadataType.RELATION,
isCustom: false,
}),
makeFieldMetadata({
name: 'company',
type: FieldMetadataType.RELATION,
isCustom: false,
}),
];
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'note',
});
const viewFieldForNoteTargets = result.flatViewFieldsToCreate.find(
(vf) =>
vf.fieldMetadataUniversalIdentifier === fields[1].universalIdentifier,
);
const viewFieldForCompany = result.flatViewFieldsToCreate.find(
(vf) =>
vf.fieldMetadataUniversalIdentifier === fields[2].universalIdentifier,
);
expect(viewFieldForNoteTargets!.isVisible).toBe(true);
expect(viewFieldForCompany!.isVisible).toBe(false);
});
it('should make activity target RELATION fields visible by default for task objects', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
isCustom: false,
}),
makeFieldMetadata({
name: 'taskTargets',
type: FieldMetadataType.RELATION,
isCustom: false,
}),
makeFieldMetadata({
name: 'company',
type: FieldMetadataType.RELATION,
isCustom: false,
}),
];
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'task',
});
const viewFieldForTaskTargets = result.flatViewFieldsToCreate.find(
(vf) =>
vf.fieldMetadataUniversalIdentifier === fields[1].universalIdentifier,
);
const viewFieldForCompany = result.flatViewFieldsToCreate.find(
(vf) =>
vf.fieldMetadataUniversalIdentifier === fields[2].universalIdentifier,
);
expect(viewFieldForTaskTargets!.isVisible).toBe(true);
expect(viewFieldForCompany!.isVisible).toBe(false);
});
it('should not make noteTargets visible on non-note objects', () => {
const fields = [
makeFieldMetadata({
name: 'noteTargets',
type: FieldMetadataType.RELATION,
isCustom: false,
}),
];
const result = computeFieldsWidgetViewFieldsAndGroupsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
objectNameSingular: 'person',
});
expect(result.flatViewFieldsToCreate[0].isVisible).toBe(false);
});
});
@@ -6,18 +6,23 @@ import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/metadata-modules/flat-view-f
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { isFieldMetadataEligibleForFieldsWidget } from 'twenty-shared/utils';
import {
isActivityTargetField,
isFieldMetadataEligibleForFieldsWidget,
} from 'twenty-shared/utils';
export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
objectFlatFieldMetadatas,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier,
objectNameSingular,
}: {
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
viewUniversalIdentifier: string;
flatApplication: FlatApplication;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
objectNameSingular: string;
}): {
flatViewFieldGroupsToCreate: UniversalFlatViewFieldGroup[];
flatViewFieldsToCreate: UniversalFlatViewField[];
@@ -38,6 +43,11 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
const standardFields = eligibleFields.filter((field) => !field.isCustom);
const customFields = eligibleFields.filter((field) => field.isCustom);
const isFieldVisible = (field: UniversalFlatFieldMetadata) =>
isActivityTargetField(field.name, objectNameSingular) ||
(field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION);
const sortedStandardFields = [...standardFields].sort((a, b) => {
const aIsLabel =
a.universalIdentifier === labelIdentifierFieldMetadataUniversalIdentifier;
@@ -71,10 +81,6 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
});
sortedStandardFields.forEach((field, index) => {
const isVisible =
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION;
flatViewFieldsToCreate.push({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
viewUniversalIdentifier,
@@ -83,7 +89,7 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: v4(),
isVisible,
isVisible: isFieldVisible(field),
isActive: true,
size: DEFAULT_VIEW_FIELD_SIZE,
position: index,
@@ -112,10 +118,6 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
});
customFields.forEach((field, index) => {
const isVisible =
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION;
flatViewFieldsToCreate.push({
fieldMetadataUniversalIdentifier: field.universalIdentifier,
viewUniversalIdentifier,
@@ -124,7 +126,7 @@ export const computeFieldsWidgetViewFieldsAndGroupsToCreate = ({
updatedAt: createdAt,
deletedAt: null,
universalIdentifier: v4(),
isVisible,
isVisible: isFieldVisible(field),
isActive: true,
size: DEFAULT_VIEW_FIELD_SIZE,
position: index,
@@ -1,5 +1,5 @@
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import { CoreObjectNameSingular } from '@/types';
import { isActivityTargetField } from '@/utils/fieldMetadata/isActivityTargetField';
describe('isActivityTargetField', () => {
it('should return true for noteTargets field on Note object', () => {
@@ -0,0 +1,10 @@
import { CoreObjectNameSingular } from '@/types';
export const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
@@ -45,6 +45,7 @@ export { CustomError } from './errors/CustomError';
export { evalFromContext } from './evalFromContext';
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name';
export { isActivityTargetField } from './fieldMetadata/isActivityTargetField';
export { isFieldMetadataArrayKind } from './fieldMetadata/isFieldMetadataArrayKind';
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
export { isFieldMetadataEligibleForFieldsWidget } from './fieldMetadata/isFieldMetadataEligibleForFieldsWidget';