Combine and clean upgrade commands for record page layouts (#19004)

- Remove the label identifier field for all standard objects as it's a
first-class citizen that is displayed specifically in the app; it
doesn't make a lot of sense to display it in the Fields widgets
- Disable logic that made the label identifier required and in first
position
- Add all fields for all standard objects in record page layout view
fields
- Do not include position and ts vector fields in custom objects

> [!IMPORTANT]
> The command will create Field widgets for all relations. It is
consistent to the way the frontend dynamically generates them as of
today. We will have to decide which relations we pin as individual Field
widgets before the release. (This will likely land in this command or in
another one.)
This commit is contained in:
Baptiste Devessier
2026-03-26 17:50:29 +01:00
committed by GitHub
parent 31718d163c
commit da1b1f1cbc
30 changed files with 3807 additions and 4472 deletions
@@ -455,6 +455,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
flatDefaultRecordPageLayoutsToCreate =
@@ -0,0 +1,209 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { computeFlatViewFieldsToCreate } from '../compute-flat-view-fields-to-create.util';
const makeFieldMetadata = (
overrides: Partial<UniversalFlatFieldMetadata> & {
name: string;
type: FieldMetadataType;
},
): UniversalFlatFieldMetadata => {
const universalIdentifier = overrides.universalIdentifier ?? v4();
return {
universalIdentifier,
objectMetadataUniversalIdentifier: 'object-uid',
applicationUniversalIdentifier: 'app-uid',
name: overrides.name,
label: overrides.label ?? overrides.name,
type: overrides.type,
isCustom: overrides.isCustom ?? false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: true,
isUnique: false,
isLabelSyncedWithName: false,
defaultValue: null,
description: null,
icon: null,
options: null,
settings: null,
standardOverrides: null,
relationTargetObjectMetadataUniversalIdentifier: null,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
deletedAt: null,
} as unknown as UniversalFlatFieldMetadata;
};
const flatApplication = {
universalIdentifier: 'app-uid',
id: 'app-id',
} as never;
const viewUniversalIdentifier = 'view-uid';
describe('computeFlatViewFieldsToCreate', () => {
it('should exclude TS_VECTOR fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'searchVector',
type: FieldMetadataType.TS_VECTOR,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude POSITION fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'position',
type: FieldMetadataType.POSITION,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude RELATION and MORPH_RELATION fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'company',
type: FieldMetadataType.RELATION,
}),
makeFieldMetadata({
name: 'target',
type: FieldMetadataType.MORPH_RELATION,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude deletedAt field', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'deletedAt',
type: FieldMetadataType.DATE_TIME,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should place label identifier field first', () => {
const labelField = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const otherField = makeFieldMetadata({
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
});
const fields = [otherField, labelField];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
});
expect(result).toHaveLength(2);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
labelField.universalIdentifier,
);
expect(result[0].position).toBe(0);
expect(result[1].fieldMetadataUniversalIdentifier).toBe(
otherField.universalIdentifier,
);
expect(result[1].position).toBe(1);
});
it('should exclude label identifier when excludeLabelIdentifier is true', () => {
const labelField = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const otherField = makeFieldMetadata({
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
});
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [labelField, otherField],
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
excludeLabelIdentifier: true,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
otherField.universalIdentifier,
);
});
});
@@ -11,22 +11,30 @@ export const computeFlatViewFieldsToCreate = ({
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier = false,
}: {
flatApplication: FlatApplication;
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
viewUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
excludeLabelIdentifier?: boolean;
}): UniversalFlatViewField[] => {
const createdAt = new Date().toISOString();
const defaultViewFields = objectFlatFieldMetadatas
.filter(
(field) =>
field.name !== 'deletedAt' &&
field.type !== FieldMetadataType.TS_VECTOR &&
field.type !== FieldMetadataType.POSITION &&
field.type !== FieldMetadataType.MORPH_RELATION &&
field.type !== FieldMetadataType.RELATION &&
// Include 'id' only if it's the label identifier (e.g., for junction tables)
(field.name !== 'id' ||
field.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier) &&
// Exclude label identifier field when requested (e.g., for FIELDS_WIDGET views)
(!excludeLabelIdentifier ||
field.universalIdentifier !==
labelIdentifierFieldMetadataUniversalIdentifier),
)
.sort((a, b) => {