Refactor read only object and fields (#13936)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773 2025-08-19 21:40:35 +05:30 committed by GitHub
parent bb7cd1baa1
commit 3ef94f6e8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 1274 additions and 483 deletions

View file

@ -42,7 +42,7 @@ npx nx g @nx/react:component my-component
},
"test": {
"executor": "@nx/jest:jest",
"options": { "jestConfig": "packages/twenty-front/jest.config.ts" }
"options": { "jestConfig": "packages/twenty-front/jest.config.mjs" }
}
}
}

2
.vscode/launch.json vendored
View file

@ -94,7 +94,7 @@
"twenty-server:jest",
"--",
"--config",
"./jest.config.ts",
"./jest.config.mjs",
"${relativeFile}"
],
"cwd": "${workspaceFolder}/packages/twenty-server",

View file

@ -104,7 +104,7 @@
],
"outputs": ["{projectRoot}/coverage"],
"options": {
"jestConfig": "{projectRoot}/jest.config.ts",
"jestConfig": "{projectRoot}/jest.config.mjs",
"coverage": true,
"coverageReporters": ["text-summary"],
"cacheDirectory": "../../.cache/jest/{projectRoot}"
@ -317,4 +317,4 @@
"tui": {
"enabled": false
}
}
}

View file

@ -1,6 +1,6 @@
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { pathsToModuleNameMapper, type JestConfigWithTsJest } from 'ts-jest';
import { pathsToModuleNameMapper } from 'ts-jest';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
@ -8,9 +8,12 @@ const __dirname = dirname(__filename);
const tsConfigPath = resolve(__dirname, './tsconfig.spec.json');
const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf8'));
// eslint-disable-next-line no-undef
process.env.TZ = 'GMT';
// eslint-disable-next-line no-undef
process.env.LC_ALL = 'en_US.UTF-8';
const jestConfig: JestConfigWithTsJest = {
const jestConfig = {
silent: true,
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1

View file

@ -640,6 +640,7 @@ export type CreateFieldInput = {
isNullable?: InputMaybe<Scalars['Boolean']>;
isRemoteCreation?: InputMaybe<Scalars['Boolean']>;
isSystem?: InputMaybe<Scalars['Boolean']>;
isUIReadOnly?: InputMaybe<Scalars['Boolean']>;
isUnique?: InputMaybe<Scalars['Boolean']>;
label: Scalars['String'];
morphRelationsCreationPayload?: InputMaybe<Array<Scalars['JSON']>>;
@ -975,6 +976,7 @@ export type Field = {
isLabelSyncedWithName?: Maybe<Scalars['Boolean']>;
isNullable?: Maybe<Scalars['Boolean']>;
isSystem?: Maybe<Scalars['Boolean']>;
isUIReadOnly?: Maybe<Scalars['Boolean']>;
isUnique?: Maybe<Scalars['Boolean']>;
label: Scalars['String'];
morphRelations?: Maybe<Array<Relation>>;
@ -1010,6 +1012,7 @@ export type FieldFilter = {
isActive?: InputMaybe<BooleanFieldComparison>;
isCustom?: InputMaybe<BooleanFieldComparison>;
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
or?: InputMaybe<Array<FieldFilter>>;
};
@ -2158,6 +2161,7 @@ export type Object = {
isRemote: Scalars['Boolean'];
isSearchable: Scalars['Boolean'];
isSystem: Scalars['Boolean'];
isUIReadOnly: Scalars['Boolean'];
labelIdentifierFieldMetadataId?: Maybe<Scalars['UUID']>;
labelPlural: Scalars['String'];
labelSingular: Scalars['String'];
@ -2212,6 +2216,7 @@ export type ObjectFilter = {
isRemote?: InputMaybe<BooleanFieldComparison>;
isSearchable?: InputMaybe<BooleanFieldComparison>;
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
or?: InputMaybe<Array<ObjectFilter>>;
};
@ -3200,6 +3205,7 @@ export type UpdateFieldInput = {
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
isNullable?: InputMaybe<Scalars['Boolean']>;
isSystem?: InputMaybe<Scalars['Boolean']>;
isUIReadOnly?: InputMaybe<Scalars['Boolean']>;
isUnique?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
@ -4226,7 +4232,7 @@ export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', dele
export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>;
export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } };
export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'ObjectEdge', node: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array<Array<string>> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } };
export type SkipBookOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
@ -7601,6 +7607,7 @@ export const ObjectMetadataItemsDocument = gql`
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
@ -7636,6 +7643,7 @@ export const ObjectMetadataItemsDocument = gql`
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt

View file

@ -636,6 +636,7 @@ export type CreateFieldInput = {
isNullable?: InputMaybe<Scalars['Boolean']>;
isRemoteCreation?: InputMaybe<Scalars['Boolean']>;
isSystem?: InputMaybe<Scalars['Boolean']>;
isUIReadOnly?: InputMaybe<Scalars['Boolean']>;
isUnique?: InputMaybe<Scalars['Boolean']>;
label: Scalars['String'];
morphRelationsCreationPayload?: InputMaybe<Array<Scalars['JSON']>>;
@ -939,6 +940,7 @@ export type Field = {
isLabelSyncedWithName?: Maybe<Scalars['Boolean']>;
isNullable?: Maybe<Scalars['Boolean']>;
isSystem?: Maybe<Scalars['Boolean']>;
isUIReadOnly?: Maybe<Scalars['Boolean']>;
isUnique?: Maybe<Scalars['Boolean']>;
label: Scalars['String'];
morphRelations?: Maybe<Array<Relation>>;
@ -974,6 +976,7 @@ export type FieldFilter = {
isActive?: InputMaybe<BooleanFieldComparison>;
isCustom?: InputMaybe<BooleanFieldComparison>;
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
or?: InputMaybe<Array<FieldFilter>>;
};
@ -2069,6 +2072,7 @@ export type Object = {
isRemote: Scalars['Boolean'];
isSearchable: Scalars['Boolean'];
isSystem: Scalars['Boolean'];
isUIReadOnly: Scalars['Boolean'];
labelIdentifierFieldMetadataId?: Maybe<Scalars['UUID']>;
labelPlural: Scalars['String'];
labelSingular: Scalars['String'];
@ -2123,6 +2127,7 @@ export type ObjectFilter = {
isRemote?: InputMaybe<BooleanFieldComparison>;
isSearchable?: InputMaybe<BooleanFieldComparison>;
isSystem?: InputMaybe<BooleanFieldComparison>;
isUIReadOnly?: InputMaybe<BooleanFieldComparison>;
or?: InputMaybe<Array<ObjectFilter>>;
};
@ -3046,6 +3051,7 @@ export type UpdateFieldInput = {
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
isNullable?: InputMaybe<Scalars['Boolean']>;
isSystem?: InputMaybe<Scalars['Boolean']>;
isUIReadOnly?: InputMaybe<Scalars['Boolean']>;
isUnique?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;

View file

@ -6,7 +6,7 @@ import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
import { msg } from '@lingui/core/macro';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
@ -81,11 +81,19 @@ export const useRelatedRecordActions = ({
selectedRecord,
objectPermissions,
getTargetObjectWritePermission,
objectMetadataItem,
}) =>
(isDefined(selectedRecord) &&
!selectedRecord.isRemote &&
isDefined(objectMetadataItem) &&
isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: objectPermissions.canUpdateObjectRecords,
objectMetadataId: objectMetadataItem.id,
},
objectMetadataItem,
isRecordDeleted: isDefined(selectedRecord.deletedAt),
}) &&
objectPermissions.canUpdateObjectRecords &&
!isWorkflowSubObjectMetadata(targetObjectNameSingular) &&
getTargetObjectWritePermission(
targetObjectNameSingular === CoreObjectNameSingular.TaskTarget
? CoreObjectNameSingular.Task

View file

@ -7,7 +7,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
@ -16,6 +15,7 @@ import { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components';
import { IconCalendarEvent } from 'twenty-ui/display';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
type CalendarEventDetailsProps = {
calendarEvent: CalendarEvent;

View file

@ -29,7 +29,7 @@ import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { isTitleCellInEditModeComponentState } from '@/object-record/record-title-cell/states/isTitleCellInEditModeComponentState';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';

View file

@ -16,6 +16,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isRemote
isActive
isSystem
isUIReadOnly
createdAt
updatedAt
labelIdentifierFieldMetadataId
@ -51,6 +52,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isCustom
isActive
isSystem
isUIReadOnly
isNullable
isUnique
createdAt

View file

@ -1,50 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
describe('isWorkflowRelatedObjectMetadata', () => {
it('should return true for Workflow object', () => {
const result = isWorkflowRelatedObjectMetadata(
CoreObjectNameSingular.Workflow,
);
expect(result).toBe(true);
});
it('should return true for WorkflowVersion object', () => {
const result = isWorkflowRelatedObjectMetadata(
CoreObjectNameSingular.WorkflowVersion,
);
expect(result).toBe(true);
});
it('should return true for WorkflowRun object', () => {
const result = isWorkflowRelatedObjectMetadata(
CoreObjectNameSingular.WorkflowRun,
);
expect(result).toBe(true);
});
it('should return false for non-workflow related objects', () => {
const result = isWorkflowRelatedObjectMetadata(
CoreObjectNameSingular.Company,
);
expect(result).toBe(false);
});
it('should return false for unknown object names', () => {
const result = isWorkflowRelatedObjectMetadata('unknownObject');
expect(result).toBe(false);
});
it('should return false for Person object', () => {
const result = isWorkflowRelatedObjectMetadata(
CoreObjectNameSingular.Person,
);
expect(result).toBe(false);
});
});

View file

@ -49,6 +49,7 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
settings: field.settings,
isNullable: field.isNullable,
isCustom: field.isCustom ?? false,
isUIReadOnly: field.isUIReadOnly ?? false,
};
return {

View file

@ -1,8 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
export const isObjectMetadataReadOnly = (
objectMetadataItem: Pick<ObjectMetadataItem, 'isRemote' | 'nameSingular'>,
) =>
objectMetadataItem.isRemote ||
isWorkflowSubObjectMetadata(objectMetadataItem.nameSingular);

View file

@ -1,9 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
export const isWorkflowRelatedObjectMetadata = (objectNameSingular: string) => {
return (
objectNameSingular === CoreObjectNameSingular.Workflow ||
isWorkflowSubObjectMetadata(objectNameSingular)
);
};

View file

@ -1,7 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
export const isWorkflowSubObjectMetadata = (
objectMetadataNameSingular?: string,
) =>
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowVersion ||
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun;

View file

@ -19,6 +19,7 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
isNullable: z.boolean(),
isUnique: z.boolean(),
isSystem: z.boolean(),
isUIReadOnly: z.boolean(),
label: metadataLabelSchema(existingLabels),
isLabelSyncedWithName: z.boolean(),
name: camelCaseStringSchema,

View file

@ -22,6 +22,7 @@ export const objectMetadataItemSchema = z.object({
isCustom: z.boolean(),
isRemote: z.boolean(),
isSystem: z.boolean(),
isUIReadOnly: z.boolean(),
isSearchable: z.boolean(),
labelIdentifierFieldMetadataId: z.string().uuid(),
labelPlural: metadataLabelSchema(),

View file

@ -53,6 +53,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
isUIReadOnly: false,
};
const Wrapper = getJestMetadataAndApolloMocksWrapper({

View file

@ -31,6 +31,7 @@ const objectMetadataItemWithPositionField: ObjectMetadataItem = {
icon: 'icon',
isActive: true,
isSystem: false,
isUIReadOnly: false,
isCustom: false,
isRemote: false,
isSearchable: false,

View file

@ -1,8 +1,8 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
export type UseFieldIsReadOnlyParams = {
fieldMetadataId: string;
@ -42,10 +42,6 @@ export const useIsRecordFieldReadOnly = ({
return isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataId,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem,
});
};

View file

@ -1,6 +1,7 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { isRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordReadOnly';
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
import { useIsRecordDeleted } from '@/object-record/record-field/ui/hooks/useIsRecordDeleted';
type UseIsRecordReadOnlyParams = {
@ -12,6 +13,10 @@ export const useIsRecordReadOnly = ({
recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectPermissions = getObjectPermissionsForObject(
@ -24,5 +29,6 @@ export const useIsRecordReadOnly = ({
return isRecordReadOnly({
objectPermissions,
isRecordDeleted,
objectMetadataItem,
});
};

View file

@ -0,0 +1,67 @@
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
describe('isObjectMetadataReadOnly', () => {
it('should return false if object can be updated and is not UI read only and is not remote', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(false);
});
it('should return true if object cannot be updated and is not UI read only and is not remote', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if object metadata is UI read only', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: true,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if object metadata is remote', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: false,
isRemote: true,
},
});
expect(result).toBe(true);
});
});

View file

@ -1,4 +1,4 @@
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('isRecordFieldReadOnly', () => {
@ -12,16 +12,18 @@ describe('isRecordFieldReadOnly', () => {
isRecordReadOnly: false,
objectPermissions: mockObjectPermissions,
fieldMetadataId: 'field-123',
objectNameSingular: 'person',
fieldName: 'firstName',
fieldType: FieldMetadataType.TEXT,
isCustom: false,
fieldMetadataType: FieldMetadataType.TEXT,
isUIReadOnly: false,
};
it('should return true when record is read-only', () => {
const result = isRecordFieldReadOnly({
...mockParams,
isRecordReadOnly: true,
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(true);
@ -34,6 +36,10 @@ describe('isRecordFieldReadOnly', () => {
...mockObjectPermissions,
canUpdateObjectRecords: false,
},
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(true);
@ -48,36 +54,22 @@ describe('isRecordFieldReadOnly', () => {
'field-123': { canUpdate: false },
},
},
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(true);
});
it('should return true for system read-only fields like createdAt', () => {
it('should return true when field is marked as UI read-only', () => {
const result = isRecordFieldReadOnly({
...mockParams,
fieldName: 'createdAt',
fieldType: FieldMetadataType.DATE_TIME,
});
expect(result).toBe(true);
});
it('should return true for calendar event objects (system read-only)', () => {
const result = isRecordFieldReadOnly({
...mockParams,
objectNameSingular: 'calendarEvent',
});
expect(result).toBe(true);
});
it('should return true for workflow non-name fields (system read-only)', () => {
const result = isRecordFieldReadOnly({
...mockParams,
objectNameSingular: 'workflow',
fieldName: 'status',
isCustom: false,
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: true,
},
});
expect(result).toBe(true);
@ -86,6 +78,10 @@ describe('isRecordFieldReadOnly', () => {
it('should return false when all conditions allow editing', () => {
const result = isRecordFieldReadOnly({
...mockParams,
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(false);

View file

@ -0,0 +1,83 @@
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
describe('isRecordReadOnly', () => {
it('should return false if record is not deleted, has update permissions and object metadata is not read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: false,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(false);
});
it('should return true if record is not deleted but lacks update permissions and object metadata is not read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: false,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if record is deleted even with update permissions and object metadata is not read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: true,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if record is not deleted and has update permissions but object metadata is UI read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: true,
objectMetadataItem: {
isUIReadOnly: true,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if record is not deleted and has update permissions but object metadata is remote', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: false,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: true,
},
});
expect(result).toBe(true);
});
});

View file

@ -0,0 +1,20 @@
import { type ObjectPermission } from '~/generated/graphql';
type IsFieldMetadataReadOnlyByPermissionParams = {
objectPermissions: ObjectPermission;
fieldMetadataId: string;
};
export const isFieldMetadataReadOnlyByPermissions = ({
objectPermissions,
fieldMetadataId,
}: IsFieldMetadataReadOnlyByPermissionParams) => {
if (objectPermissions.canUpdateObjectRecords === false) {
return true;
}
const fieldMetadataIsRestrictedForUpdate =
objectPermissions.restrictedFields?.[fieldMetadataId]?.canUpdate === false;
return fieldMetadataIsRestrictedForUpdate;
};

View file

@ -0,0 +1,33 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldMetadataReadOnlyByPermissions } from '@/object-record/read-only/utils/internal/isFieldMetadataReadOnlyByPermissions';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { type ObjectPermission } from '~/generated/graphql';
type IsFieldMetadataReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
fieldMetadataItem: Pick<FieldMetadataItem, 'id' | 'isUIReadOnly'>;
};
export const isFieldMetadataReadOnly = ({
objectPermissions,
objectMetadataItem,
fieldMetadataItem,
}: IsFieldMetadataReadOnlyParams) => {
const objectMetadataReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const fieldReadOnlyByPermissions = isFieldMetadataReadOnlyByPermissions({
objectPermissions,
fieldMetadataId: fieldMetadataItem.id,
});
return (
objectMetadataReadOnly ||
fieldMetadataItem.isUIReadOnly ||
fieldReadOnlyByPermissions
);
};

View file

@ -0,0 +1,18 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectPermission } from '~/generated/graphql';
type IsObjectMetadataReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
};
export const isObjectMetadataReadOnly = ({
objectPermissions,
objectMetadataItem,
}: IsObjectMetadataReadOnlyParams) => {
return (
!objectPermissions.canUpdateObjectRecords ||
objectMetadataItem.isUIReadOnly ||
objectMetadataItem.isRemote
);
};

View file

@ -0,0 +1,26 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isFieldMetadataReadOnlyByPermissions } from '@/object-record/read-only/utils/internal/isFieldMetadataReadOnlyByPermissions';
import { type ObjectPermission } from '~/generated/graphql';
type IsRecordFieldReadOnlyParams = {
isRecordReadOnly: boolean;
fieldMetadataItem: Pick<FieldMetadataItem, 'id' | 'isUIReadOnly'>;
objectPermissions: ObjectPermission;
};
export const isRecordFieldReadOnly = ({
objectPermissions,
isRecordReadOnly,
fieldMetadataItem,
}: IsRecordFieldReadOnlyParams) => {
const fieldReadOnlyByPermissions = isFieldMetadataReadOnlyByPermissions({
objectPermissions,
fieldMetadataId: fieldMetadataItem.id,
});
return (
isRecordReadOnly ||
fieldMetadataItem.isUIReadOnly ||
fieldReadOnlyByPermissions
);
};

View file

@ -0,0 +1,23 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { type ObjectPermission } from '~/generated/graphql';
export type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
isRecordDeleted: boolean;
};
export const isRecordReadOnly = ({
objectPermissions,
isRecordDeleted,
objectMetadataItem,
}: IsObjectReadOnlyParams) => {
return (
isRecordDeleted ||
isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
})
);
};

View file

@ -1,3 +1,4 @@
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
@ -10,7 +11,6 @@ import {
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/ui/utils/getFieldButtonIcon';
@ -45,12 +45,10 @@ export const RecordBoardCardBody = ({
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataId: fieldDefinition.fieldMetadataId,
fieldName: fieldDefinition.metadata.fieldName,
fieldType: fieldDefinition.type,
isCustom: fieldDefinition.metadata.isCustom,
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular ?? '',
fieldMetadataItem: {
id: fieldDefinition.fieldMetadataId,
isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false,
},
}),
}),
);

View file

@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd';
import { useContext } from 'react';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
import { RecordBoardCardHotkeysEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect';
@ -9,7 +10,6 @@ import { RecordBoardCardMultiDragPreview } from '@/object-record/record-board/re
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
const StyledDraggableContainer = styled.div`

View file

@ -35,6 +35,7 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isRemote: false,
isSearchable: false,
labelIdentifierFieldMetadataId: '06b33746-5293-4d07-9f7f-ebf5ad396064',

View file

@ -3,6 +3,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldListCellEditModePortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellEditModePortal';
import { RecordFieldListCellHoveredPortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
@ -11,8 +13,6 @@ import { RecordDetailRelationSection } from '@/object-record/record-field-list/r
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
@ -123,11 +123,10 @@ export const RecordFieldList = ({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
}}
>
@ -172,11 +171,10 @@ export const RecordFieldList = ({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
onMouseEnter: () =>
handleMouseEnter(
@ -233,11 +231,10 @@ export const RecordFieldList = ({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
}}
>

View file

@ -1,10 +1,10 @@
import { useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { RecordDetailRelationSectionDropdownToMany } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany';
import { RecordDetailRelationSectionDropdownToOne } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RelationType } from '~/generated-metadata/graphql';

View file

@ -1,12 +1,12 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import {
FieldContext,
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { type ReactNode } from 'react';
export const FieldContextProvider = ({

View file

@ -1,25 +0,0 @@
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
describe('isObjectReadOnly', () => {
it('should return true if object is not read only', () => {
const result = isObjectReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
});
expect(result).toBe(false);
});
it('should return false if object is read only', () => {
const result = isObjectReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
});
expect(result).toBe(true);
});
});

View file

@ -1,51 +0,0 @@
import { isRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordReadOnly';
describe('isRecordReadOnly', () => {
it('should return false if record is not deleted and has update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: false,
});
expect(result).toBe(false);
});
it('should return true if record is not deleted but lacks update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: false,
});
expect(result).toBe(true);
});
it('should return true if record is deleted even with update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: true,
});
expect(result).toBe(true);
});
it('should return true if record is deleted and lacks update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: true,
});
expect(result).toBe(true);
});
});

View file

@ -1,21 +0,0 @@
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
import { type ObjectPermission } from '~/generated/graphql';
export type IsFieldReadOnlyByPermissionParams = {
objectPermissions: ObjectPermission;
fieldMetadataId: string;
};
export const isFieldReadOnlyByPermissions = ({
objectPermissions,
fieldMetadataId,
}: IsFieldReadOnlyByPermissionParams) => {
if (isObjectReadOnly({ objectPermissions }) === true) {
return true;
}
const fieldMetadataIsRestrictedForUpdate =
objectPermissions.restrictedFields[fieldMetadataId]?.canUpdate === false;
return fieldMetadataIsRestrictedForUpdate;
};

View file

@ -1,80 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isWorkflowRunJsonField } from '@/object-record/record-field/ui/meta-types/utils/isWorkflowRunJsonField';
import { isFieldActor } from '@/object-record/record-field/ui/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type IsFieldReadOnlyBySystemParams = {
objectNameSingular: string;
fieldName?: string;
fieldType?: FieldMetadataType;
isCustom?: boolean;
};
export const isFieldReadOnlyBySystem = ({
objectNameSingular,
fieldName,
fieldType,
isCustom,
}: IsFieldReadOnlyBySystemParams) => {
if (
isWorkflowRunJsonField({
objectMetadataNameSingular: objectNameSingular,
fieldName,
})
) {
return false;
}
if (isWorkflowSubObjectMetadata(objectNameSingular) && !isCustom) {
return true;
}
if (objectNameSingular === CoreObjectNameSingular.CalendarEvent) {
return true;
}
if (
objectNameSingular === CoreObjectNameSingular.Workflow &&
fieldName !== 'name' &&
!isCustom
) {
return true;
}
if (
objectNameSingular !== CoreObjectNameSingular.Note &&
fieldName === 'noteTargets'
) {
return true;
}
if (
objectNameSingular !== CoreObjectNameSingular.Task &&
fieldName === 'taskTargets'
) {
return true;
}
const isFieldDateOrDateTime =
fieldType === FieldMetadataType.DATE ||
fieldType === FieldMetadataType.DATE_TIME;
const isFieldCreatedAtOrUpdatedAt =
fieldName === 'createdAt' || fieldName === 'updatedAt';
if (isFieldDateOrDateTime && isFieldCreatedAtOrUpdatedAt) {
return true;
}
if (
isDefined(fieldType) &&
(isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
) {
return true;
}
return false;
};

View file

@ -1,11 +0,0 @@
import { type ObjectPermission } from '~/generated/graphql';
type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
};
export const isObjectReadOnly = ({
objectPermissions,
}: IsObjectReadOnlyParams) => {
return !objectPermissions.canUpdateObjectRecords;
};

View file

@ -1,39 +0,0 @@
import {
isFieldReadOnlyByPermissions,
type IsFieldReadOnlyByPermissionParams,
} from '@/object-record/record-field/ui/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions';
import {
isFieldReadOnlyBySystem,
type IsFieldReadOnlyBySystemParams,
} from '@/object-record/record-field/ui/hooks/read-only/utils/internal/isFieldReadOnlyBySystem';
type IsRecordFieldReadOnlyParams = {
isRecordReadOnly: boolean;
} & IsFieldReadOnlyByPermissionParams &
IsFieldReadOnlyBySystemParams;
export const isRecordFieldReadOnly = ({
isRecordReadOnly,
objectPermissions,
fieldMetadataId,
objectNameSingular,
fieldName,
fieldType,
isCustom,
}: IsRecordFieldReadOnlyParams) => {
const fieldReadOnlyByPermissions = isFieldReadOnlyByPermissions({
objectPermissions,
fieldMetadataId,
});
const fieldReadOnlyBySystem = isFieldReadOnlyBySystem({
objectNameSingular,
fieldName,
fieldType,
isCustom,
});
return (
isRecordReadOnly || fieldReadOnlyByPermissions || fieldReadOnlyBySystem
);
};

View file

@ -1,13 +0,0 @@
import { type ObjectPermission } from '~/generated/graphql';
type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
isRecordDeleted: boolean;
};
export const isRecordReadOnly = ({
objectPermissions,
isRecordDeleted,
}: IsObjectReadOnlyParams) => {
return isRecordDeleted || !objectPermissions.canUpdateObjectRecords;
};

View file

@ -29,7 +29,6 @@ import { recordStoreFamilySelector } from '@/object-record/record-store/states/s
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { isWorkflowRunJsonField } from '@/object-record/record-field/ui/meta-types/utils/isWorkflowRunJsonField';
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
import { isFieldArrayValue } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
@ -143,13 +142,10 @@ export const usePersistField = ({
const fieldIsArray =
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);
const isUnpersistableRawJsonField = isWorkflowRunJsonField({
objectMetadataNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
fieldName: fieldDefinition.metadata.fieldName,
});
const fieldIsUIReadOnly =
fieldDefinition.metadata.isUIReadOnly ?? false;
if (fieldIsRawJson && isUnpersistableRawJsonField) {
if (fieldIsRawJson && fieldIsUIReadOnly) {
return;
}

View file

@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { isWorkflowRunJsonField } from '@/object-record/record-field/ui/meta-types/utils/isWorkflowRunJsonField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
@ -142,11 +141,7 @@ export const RawJsonFieldInput = () => {
dependencies: [handleShiftTab, draftValue],
});
const showEditingButton = !isWorkflowRunJsonField({
objectMetadataNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
fieldName: fieldDefinition.metadata.fieldName,
});
const showEditingButton = !fieldDefinition.metadata.isUIReadOnly;
const handleStartEditing = () => {
setIsEditing(true);

View file

@ -1,15 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
// FIXME: This is temporary. We'll soon introduce a new display mode for all fields and we'll have to remove this code.
export const isWorkflowRunJsonField = ({
objectMetadataNameSingular,
fieldName,
}: {
fieldName: string | undefined;
objectMetadataNameSingular: string | undefined;
}) => {
return (
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun &&
fieldName === 'state'
);
};

View file

@ -15,4 +15,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
infoTooltipContent?: string;
defaultValue?: any;
editButtonIcon?: IconComponent;
isUIReadOnly?: boolean;
};

View file

@ -15,6 +15,7 @@ type BaseFieldMetadata = {
fieldName: string;
objectMetadataNameSingular?: string;
isCustom?: boolean;
isUIReadOnly?: boolean;
};
export type FieldUuidMetadata = BaseFieldMetadata & {

View file

@ -132,6 +132,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
isRemote: false,
isSearchable: true,
isSystem: false,
isUIReadOnly: false,
labelIdentifierFieldMetadataId: 'mock-id',
labelPlural: 'Tests',
labelSingular: 'Test',

View file

@ -224,6 +224,7 @@ describe('useRecordData', () => {
fieldName: 'updatedAt',
isCustom: false,
isNullable: false,
isUIReadOnly: false,
objectMetadataNameSingular: 'person',
options: null,
placeHolder: 'Last update',

View file

@ -2,13 +2,13 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import {
FieldContext,
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/ui/contexts/FieldFocusContextProvider';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCellAnchoredPortalContext } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortalContext';
import { RecordInlineCellCloseOnCommandMenuOpeningEffect } from '@/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect';

View file

@ -1,8 +1,8 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';

View file

@ -2,8 +2,8 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';

View file

@ -1,4 +1,5 @@
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { type IconComponent } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@ -35,7 +36,13 @@ export const RecordTableEmptyStateDisplay = (
props: RecordTableEmptyStateDisplayProps,
) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
return (
<AnimatedPlaceholderEmptyContainer>

View file

@ -1,7 +1,7 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@ -63,11 +63,10 @@ export const RecordTableCellFieldContextGeneric = ({
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
objectPermissions,
fieldMetadataId: columnDefinition.fieldMetadataId,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: columnDefinition.metadata.fieldName,
fieldType: columnDefinition.type,
isCustom: objectMetadataItem.isCustom,
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
isUIReadOnly: columnDefinition.metadata.isUIReadOnly ?? false,
},
}),
isForbidden: !hasObjectReadPermissions,
}}

View file

@ -1,6 +1,6 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
@ -70,13 +70,12 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
isLabelIdentifierCompact,
displayedMaxRows: 1,
isRecordFieldReadOnly: isRecordFieldReadOnly({
objectPermissions,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: columnDefinition.metadata.fieldName,
fieldType: columnDefinition.type,
isCustom: objectMetadataItem.isCustom,
fieldMetadataId: columnDefinition.fieldMetadataId,
isRecordReadOnly: isRecordReadOnly ?? false,
objectPermissions,
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
isUIReadOnly: columnDefinition.metadata.isUIReadOnly ?? false,
},
}),
maxWidth: columnDefinition.size,
onRecordChipClick: () => {

View file

@ -1,6 +1,6 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';

View file

@ -2,8 +2,8 @@ import styled from '@emotion/styled';
import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
@ -241,8 +241,9 @@ export const RecordTableHeaderCell = ({
createNewIndexRecord();
};
const isReadOnly = isObjectReadOnly({
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;

View file

@ -1,5 +1,5 @@
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';

View file

@ -22,6 +22,7 @@ describe('generateAggregateQuery', () => {
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
isUIReadOnly: false,
};
const mockRecordGqlFields = {
@ -63,6 +64,7 @@ describe('generateAggregateQuery', () => {
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
isUIReadOnly: false,
};
const mockRecordGqlFields = {

View file

@ -15,11 +15,7 @@ import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { v4 } from 'uuid';
import {
FieldMetadataType,
type FieldPermission,
RelationType,
} from '~/generated/graphql';
import { type FieldPermission, RelationType } from '~/generated/graphql';
export const StyledObjectFieldTableRow = styled(TableRow)`
grid-template-columns: 180px 1fr 60px 60px;
@ -75,21 +71,6 @@ export const SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow =
const { upsertFieldPermissionInDraftRole } =
useUpsertFieldPermissionInDraftRole(roleId);
const fieldIsCreatedBy =
fieldMetadataItem.name === 'createdBy' &&
fieldMetadataItem.type === FieldMetadataType.ACTOR;
const fieldIsDeletedAt =
fieldMetadataItem.name === 'deletedAt' &&
fieldMetadataItem.type === FieldMetadataType.DATE_TIME;
const fieldIsLabelIdentifier =
fieldMetadataItem.id ===
objectMetadataItem.labelIdentifierFieldMetadataId;
const fieldMustBeReadableAndUpdatable =
fieldIsLabelIdentifier || fieldIsCreatedBy || fieldIsDeletedAt;
const handleSeeChange = () => {
if (isDefined(fieldPermissionForThisFieldMetadataItem)) {
if (
@ -196,7 +177,7 @@ export const SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow =
) : (
<TableCell>
<OverridableCheckbox
disabled={fieldMustBeReadableAndUpdatable ?? false}
disabled={fieldMetadataItem.isUIReadOnly ?? false}
checked={true}
onChange={handleSeeChange}
type={isReadRestricted ? 'override' : 'default'}
@ -208,7 +189,7 @@ export const SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow =
) : (
<TableCell align="left">
<OverridableCheckbox
disabled={fieldMustBeReadableAndUpdatable ?? false}
disabled={fieldMetadataItem.isUIReadOnly ?? false}
checked={true}
onChange={handleUpdateChange}
type={isUpdateRestricted ? 'override' : 'default'}

View file

@ -1,13 +1,11 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
export const useObjectMetadataItemsThatCanHavePermission = () => {
const { alphaSortedActiveNonSystemObjectMetadataItems: objectMetadataItems } =
useFilteredObjectMetadataItems();
const objectMetadataItemsThatCanHavePermission = objectMetadataItems.filter(
(objectMetadataItem) =>
!isWorkflowRelatedObjectMetadata(objectMetadataItem.nameSingular),
(objectMetadataItem) => !objectMetadataItem.isUIReadOnly,
);
return {

View file

@ -47,6 +47,7 @@ export const mapViewFieldsToColumnDefinitions = ({
isLabelIdentifier,
isVisible: isLabelIdentifier || viewField.isVisible,
viewFieldId: viewField.id,
isUIReadOnly: correspondingColumnDefinition.metadata.isUIReadOnly,
isSortable: correspondingColumnDefinition.isSortable,
isFilterable: correspondingColumnDefinition.isFilterable,
defaultValue: correspondingColumnDefinition.defaultValue,

View file

@ -25,6 +25,7 @@ const fields = [
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: false,
createdAt: '',
updatedAt: '',
@ -38,6 +39,7 @@ const fields = [
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: true,
createdAt: '',
updatedAt: '',
@ -51,6 +53,7 @@ const fields = [
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: true,
createdAt: '',
updatedAt: '',
@ -66,6 +69,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
description: 'A company',
icon: 'IconBuilding',
isSystem: false,
isUIReadOnly: false,
isCustom: false,
isActive: true,
createdAt: '',

View file

@ -10,7 +10,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"lingui.config.ts",
"jest.config.ts",
"jest.config.mjs",
"vite.config.ts",
"setupTests.ts"
],

View file

@ -10,7 +10,7 @@
},
"include": [
"**/__mocks__/**/*",
"jest.config.ts",
"jest.config.mjs",
"setupTests.ts",
"src/**/*.d.ts",
"src/**/*.spec.ts",

View file

@ -0,0 +1,27 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddIsUIReadOnlyToFieldMetadata1755000000000
implements MigrationInterface
{
name = 'AddIsUIReadOnlyToFieldMetadata1755000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ADD "isUIReadOnly" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "core"."objectMetadata" ADD "isUIReadOnly" boolean NOT NULL DEFAULT false`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" DROP COLUMN "isUIReadOnly"`,
);
await queryRunner.query(
`ALTER TABLE "core"."objectMetadata" DROP COLUMN "isUIReadOnly"`,
);
}
}

View file

@ -108,6 +108,11 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
@FilterableField({ nullable: true })
isSystem?: boolean;
@IsBoolean()
@IsOptional()
@FilterableField({ nullable: true })
isUIReadOnly?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })

View file

@ -103,6 +103,9 @@ export class FieldMetadataEntity<
@Column({ default: false })
isSystem: boolean;
@Column({ default: false })
isUIReadOnly: boolean;
// Is this really nullable ?
@Column({ nullable: true, default: true, type: 'boolean' })
isNullable: boolean | null;

View file

@ -30,6 +30,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
label: 'flat field metadata label',
isNullable: true,
isUnique: false,
isUIReadOnly: false,
isLabelSyncedWithName: false,
isSystem: false,
standardId: null,

View file

@ -53,5 +53,6 @@ export const getDefaultFlatFieldMetadata = ({
settings: settings ?? null,
createdAt,
updatedAt: createdAt,
isUIReadOnly: createFieldInput.isUIReadOnly ?? false,
} as const satisfies FlatFieldMetadata;
};

View file

@ -25,6 +25,7 @@ export const getFlatObjectMetadataMock = (
isRemote: false,
isSearchable: true,
isSystem: false,
isUIReadOnly: false,
labelIdentifierFieldMetadataId: faker.string.uuid(),
labelPlural: 'default flat object metadata label plural',
labelSingular: 'default flat object metadata label singular',

View file

@ -63,6 +63,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
isLabelSyncedWithName: createObjectInput.isLabelSyncedWithName ?? false,
isRemote: createObjectInput.isRemote ?? false,
isSearchable: true,
isUIReadOnly: false,
isSystem: false,
labelIdentifierFieldMetadataId: baseCustomFlatFieldMetadatas.nameField.id,
labelPlural: capitalize(createObjectInput.labelPlural),

View file

@ -71,6 +71,9 @@ export class ObjectMetadataDTO {
@FilterableField()
isSystem: boolean;
@FilterableField()
isUIReadOnly: boolean;
@FilterableField()
isSearchable: boolean;

View file

@ -76,6 +76,9 @@ export class ObjectMetadataEntity implements Required<ObjectMetadataEntity> {
@Column({ default: false })
isSystem: boolean;
@Column({ default: false })
isUIReadOnly: boolean;
@Column({ default: true })
isAuditLogged: boolean;

View file

@ -22,6 +22,7 @@ export const buildDefaultFieldsForCustomObject = (
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: true,
workspaceId,
defaultValue: 'uuid',
},
@ -36,6 +37,7 @@ export const buildDefaultFieldsForCustomObject = (
isNullable: false,
isActive: true,
isCustom: false,
isUIReadOnly: false,
workspaceId,
defaultValue: "'Untitled'",
},
@ -50,6 +52,7 @@ export const buildDefaultFieldsForCustomObject = (
isNullable: false,
isActive: true,
isCustom: false,
isUIReadOnly: false,
workspaceId,
defaultValue: 'now',
},
@ -65,6 +68,7 @@ export const buildDefaultFieldsForCustomObject = (
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: false,
workspaceId,
defaultValue: 'now',
},
@ -80,6 +84,7 @@ export const buildDefaultFieldsForCustomObject = (
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
workspaceId,
defaultValue: null,
},
@ -95,6 +100,7 @@ export const buildDefaultFieldsForCustomObject = (
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
workspaceId,
defaultValue: { name: "''", source: "'MANUAL'" },
},
@ -110,6 +116,7 @@ export const buildDefaultFieldsForCustomObject = (
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: false,
workspaceId,
defaultValue: 0,
},

View file

@ -35,6 +35,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: true,
defaultValue: 'uuid',
createdAt,
@ -65,6 +66,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: false,
defaultValue: "'Untitled'",
createdAt,
@ -95,6 +97,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
defaultValue: 'now',
createdAt,
@ -125,6 +128,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
defaultValue: 'now',
createdAt,
@ -155,6 +159,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
defaultValue: null,
createdAt,
@ -185,6 +190,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
defaultValue: { name: "''", source: "'MANUAL'" },
createdAt,
@ -215,6 +221,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: true,
defaultValue: 0,
createdAt,
@ -245,6 +252,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: true,
defaultValue: null,
createdAt,

View file

@ -75,6 +75,7 @@ const generateSourceFlatFieldMetadata = ({
isCustom: false,
isLabelSyncedWithName: false,
isNullable: true,
isUIReadOnly: false,
isSystem: true,
isUnique: false,
label: capitalize(targetFlatObjectMetadata.namePlural),
@ -133,6 +134,7 @@ const generateTargetFlatFieldMetadata = ({
isCustom: false,
isActive: true,
isSystem: true,
isUIReadOnly: false,
type: FieldMetadataType.RELATION,
icon: 'IconBuildingSkyscraper',
isNullable: true,

View file

@ -4,6 +4,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { DateDisplayFormat } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsPrimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -19,6 +20,7 @@ export abstract class BaseWorkspaceEntity {
icon: 'Icon123',
})
@WorkspaceIsPrimaryField()
@WorkspaceIsFieldUIReadOnly()
@WorkspaceIsSystem()
id: string;
@ -33,6 +35,7 @@ export abstract class BaseWorkspaceEntity {
displayFormat: DateDisplayFormat.RELATIVE,
},
})
@WorkspaceIsFieldUIReadOnly()
createdAt: string;
@WorkspaceField({
@ -46,6 +49,7 @@ export abstract class BaseWorkspaceEntity {
displayFormat: DateDisplayFormat.RELATIVE,
},
})
@WorkspaceIsFieldUIReadOnly()
updatedAt: string;
@WorkspaceField({
@ -59,5 +63,6 @@ export abstract class BaseWorkspaceEntity {
},
})
@WorkspaceIsNullable()
@WorkspaceIsFieldUIReadOnly()
deletedAt: string | null;
}

View file

@ -12,6 +12,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@ -52,6 +53,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
defaultValue: 0,
})
@WorkspaceIsSystem()
@WorkspaceIsFieldUIReadOnly()
position: number;
@WorkspaceField({
@ -61,6 +63,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceRelation({
@ -158,6 +161,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsNullable()
@WorkspaceIsSystem()
@WorkspaceIsFieldUIReadOnly()
@WorkspaceFieldIndex({ indexType: IndexType.GIN })
searchVector: string;
}

View file

@ -42,6 +42,11 @@ export function WorkspaceEntity(
'workspace:is-searchable-metadata-args',
target,
) ?? false;
const isUIReadOnly =
TypedReflect.getMetadata(
'workspace:is-object-ui-readonly-metadata-args',
target,
) ?? false;
const objectName = convertClassNameToObjectMetadataName(target.name);
@ -60,6 +65,7 @@ export function WorkspaceEntity(
shortcut: options.shortcut,
isAuditLogged,
isSystem,
isUIReadOnly,
gate,
duplicateCriteria,
isSearchable,

View file

@ -52,6 +52,12 @@ export function WorkspaceField<T extends FieldMetadataType>(
object,
propertyKey.toString(),
) ?? false;
const isUIReadOnly =
TypedReflect.getMetadata(
'workspace:is-field-ui-readonly-metadata-args',
object,
propertyKey.toString(),
) ?? false;
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
object,
@ -91,6 +97,7 @@ export function WorkspaceField<T extends FieldMetadataType>(
isPrimary,
isNullable,
isSystem,
isUIReadOnly,
gate,
isDeprecated,
isUnique,

View file

@ -0,0 +1,16 @@
import { TypedReflect } from 'src/utils/typed-reflect';
export function WorkspaceIsFieldUIReadOnly() {
return function (target: object, propertyKey?: string | symbol): void {
if (propertyKey === undefined) {
throw new Error('This decorator should be used with a field not a class');
}
TypedReflect.defineMetadata(
'workspace:is-field-ui-readonly-metadata-args',
true,
target,
propertyKey.toString(),
);
};
}

View file

@ -0,0 +1,11 @@
import { TypedReflect } from 'src/utils/typed-reflect';
export function WorkspaceIsObjectUIReadOnly() {
return function (target: object): void {
TypedReflect.defineMetadata(
'workspace:is-object-ui-readonly-metadata-args',
true,
target,
);
};
}

View file

@ -58,6 +58,12 @@ export function WorkspaceRelation<TClass extends object>(
object,
propertyKey.toString(),
) ?? false;
const isUIReadOnly =
TypedReflect.getMetadata(
'workspace:is-field-ui-readonly-metadata-args',
object,
propertyKey.toString(),
) ?? false;
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
object,
@ -89,6 +95,7 @@ export function WorkspaceRelation<TClass extends object>(
isPrimary,
isNullable,
isSystem,
isUIReadOnly,
gate,
isLabelSyncedWithName,
});

View file

@ -53,6 +53,11 @@ export interface WorkspaceEntityMetadataArgs {
*/
readonly isSystem: boolean;
/**
* Is UI read-only object.
*/
readonly isUIReadOnly: boolean;
/**
* Entity gate.
*/

View file

@ -71,6 +71,11 @@ export interface WorkspaceFieldMetadataArgs {
*/
readonly isSystem: boolean;
/**
* Is UI read-only field.
*/
readonly isUIReadOnly: boolean;
/**
* Is nullable field.
*/

View file

@ -70,6 +70,11 @@ export interface WorkspaceRelationMetadataArgs {
*/
readonly isSystem: boolean;
/**
* Is UI read-only field.
*/
readonly isUIReadOnly: boolean;
/**
* Is nullable field.
*/

View file

@ -28,6 +28,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "flat field metadata label",
"name": "flatFieldMetadataName",
@ -66,6 +67,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "flat field metadata label",
"name": "flatFieldMetadataName",
@ -95,6 +97,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isRemote": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataId": Any<String>,
"labelPlural": "Rockets",
"labelSingular": "Rocket",
@ -115,6 +118,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "flat field metadata label",
"name": "flatFieldMetadataName",
@ -152,6 +156,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "flat field metadata label",
"name": "flatFieldMetadataName",
@ -181,6 +186,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isRemote": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataId": Any<String>,
"labelPlural": "Rockets",
"labelSingular": "Rocket",
@ -201,6 +207,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "flat field metadata label",
"name": "flatFieldMetadataName",
@ -230,6 +237,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isRemote": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataId": Any<String>,
"labelPlural": "Pets",
"labelSingular": "Pet",
@ -250,6 +258,7 @@ exports[`Workspace migration builder field actions test suite It should build an
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "flat field metadata label",
"name": "flatFieldMetadataName",
@ -416,6 +425,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Id",
"name": "id",
@ -447,6 +457,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Name",
"name": "name",
@ -478,6 +489,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Creation date",
"name": "createdAt",
@ -509,6 +521,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Last update",
"name": "updatedAt",
@ -540,6 +553,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Deleted at",
"name": "deletedAt",
@ -574,6 +588,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Created by",
"name": "createdBy",
@ -605,6 +620,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Position",
"name": "position",
@ -640,6 +656,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -667,6 +684,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "TimelineActivities",
"name": "timelineActivities",
@ -704,6 +722,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -731,6 +750,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Favorites",
"name": "favorites",
@ -768,6 +788,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -795,6 +816,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Attachments",
"name": "attachments",
@ -832,6 +854,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -859,6 +882,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "NoteTargets",
"name": "noteTargets",
@ -896,6 +920,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -923,6 +948,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "TaskTargets",
"name": "taskTargets",
@ -956,6 +982,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Search vector",
"name": "searchVector",
@ -988,6 +1015,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isRemote": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataId": Any<String>,
"labelPlural": "Rockets",
"labelSingular": "Rocket",
@ -1028,6 +1056,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Pet",
"name": "pet",
@ -1055,6 +1084,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "NoteTargets",
"name": "noteTargets",
@ -1088,6 +1118,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Id",
"name": "id",
@ -1119,6 +1150,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Name",
"name": "name",
@ -1150,6 +1182,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Creation date",
"name": "createdAt",
@ -1181,6 +1214,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Last update",
"name": "updatedAt",
@ -1212,6 +1246,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Deleted at",
"name": "deletedAt",
@ -1246,6 +1281,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Created by",
"name": "createdBy",
@ -1277,6 +1313,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Position",
"name": "position",
@ -1312,6 +1349,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Pet",
"name": "pet",
@ -1339,6 +1377,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "TimelineActivities",
"name": "timelineActivities",
@ -1376,6 +1415,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Pet",
"name": "pet",
@ -1403,6 +1443,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Favorites",
"name": "favorites",
@ -1440,6 +1481,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Pet",
"name": "pet",
@ -1467,6 +1509,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Attachments",
"name": "attachments",
@ -1504,6 +1547,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Pet",
"name": "pet",
@ -1531,6 +1575,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "TaskTargets",
"name": "taskTargets",
@ -1564,6 +1609,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Search vector",
"name": "searchVector",
@ -1595,6 +1641,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Species",
"name": "species",
@ -1669,6 +1716,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Traits",
"name": "traits",
@ -1743,6 +1791,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Comments",
"name": "comments",
@ -1774,6 +1823,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Age",
"name": "age",
@ -1814,6 +1864,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Location",
"name": "location",
@ -1850,6 +1901,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Vet phone",
"name": "vetPhone",
@ -1884,6 +1936,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Vet email",
"name": "vetEmail",
@ -1915,6 +1968,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Birthday",
"name": "birthday",
@ -1946,6 +2000,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Is good with kids",
"name": "isGoodWithKids",
@ -1981,6 +2036,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Pictures",
"name": "pictures",
@ -2015,6 +2071,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Average cost of kibble per month",
"name": "averageCostOfKibblePerMonth",
@ -2049,6 +2106,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Makes its owner think of",
"name": "makesOwnerThinkOf",
@ -2080,6 +2138,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Sound swag (bark style, meow style, etc.)",
"name": "soundSwag",
@ -2142,6 +2201,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Bio",
"name": "bio",
@ -2173,6 +2233,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Interesting facts",
"name": "interestingFacts",
@ -2204,6 +2265,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Extra data",
"name": "extraData",
@ -2236,6 +2298,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isRemote": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataId": Any<String>,
"labelPlural": "Pets",
"labelSingular": "Pet",
@ -2272,6 +2335,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Id",
"name": "id",
@ -2303,6 +2367,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Name",
"name": "name",
@ -2334,6 +2399,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Creation date",
"name": "createdAt",
@ -2365,6 +2431,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Last update",
"name": "updatedAt",
@ -2396,6 +2463,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Deleted at",
"name": "deletedAt",
@ -2430,6 +2498,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"label": "Created by",
"name": "createdBy",
@ -2461,6 +2530,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": false,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Position",
"name": "position",
@ -2496,6 +2566,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -2523,6 +2594,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "TimelineActivities",
"name": "timelineActivities",
@ -2560,6 +2632,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -2587,6 +2660,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Favorites",
"name": "favorites",
@ -2624,6 +2698,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -2651,6 +2726,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Attachments",
"name": "attachments",
@ -2688,6 +2764,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -2715,6 +2792,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "NoteTargets",
"name": "noteTargets",
@ -2752,6 +2830,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Rocket",
"name": "rocket",
@ -2779,6 +2858,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "TaskTargets",
"name": "taskTargets",
@ -2812,6 +2892,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Search vector",
"name": "searchVector",
@ -2844,6 +2925,7 @@ exports[`Workspace migration builder object actions test suite It should build a
"isRemote": false,
"isSearchable": true,
"isSystem": false,
"isUIReadOnly": false,
"labelIdentifierFieldMetadataId": Any<String>,
"labelPlural": "Rockets",
"labelSingular": "Rocket",

View file

@ -153,6 +153,7 @@ export class StandardFieldFactory {
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
isSystem: workspaceFieldMetadataArgs.isSystem ?? false,
isActive: workspaceFieldMetadataArgs.isActive ?? true,
isUIReadOnly: workspaceFieldMetadataArgs.isUIReadOnly ?? false,
asExpression: workspaceFieldMetadataArgs.asExpression,
generatedType: workspaceFieldMetadataArgs.generatedType,
isLabelSyncedWithName: workspaceFieldMetadataArgs.isLabelSyncedWithName,
@ -195,6 +196,7 @@ export class StandardFieldFactory {
isSystem:
workspaceEntityMetadataArgs?.isSystem ||
workspaceRelationMetadataArgs.isSystem,
isUIReadOnly: workspaceRelationMetadataArgs.isUIReadOnly,
isNullable: true,
isUnique: false,
isActive: workspaceRelationMetadataArgs.isActive ?? true,

View file

@ -49,6 +49,7 @@ export const computeStandardFields = (
isNullable: true,
isLabelSyncedWithName: true,
isUnique: null,
isUIReadOnly: false,
options: null,
relationTargetFieldMetadata: null,
relationTargetFieldMetadataId: null,

View file

@ -11,6 +11,7 @@ import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-enti
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsObjectUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-object-ui-readonly.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
import { CALENDAR_EVENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
@ -30,6 +31,7 @@ import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/co
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
@WorkspaceIsObjectUIReadOnly()
export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.title,

View file

@ -17,6 +17,7 @@ import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-enti
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -157,6 +158,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
// Relations
@ -197,6 +199,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => TaskTargetWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
taskTargets: Relation<TaskTargetWorkspaceEntity[]>;
@WorkspaceRelation({
@ -208,6 +211,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => NoteTargetWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
noteTargets: Relation<NoteTargetWorkspaceEntity[]>;
@WorkspaceRelation({

View file

@ -13,6 +13,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -86,6 +87,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceRelation({

View file

@ -14,6 +14,7 @@ import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-enti
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -122,6 +123,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceRelation({
@ -178,6 +180,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => TaskTargetWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
taskTargets: Relation<TaskTargetWorkspaceEntity[]>;
@WorkspaceRelation({
@ -189,6 +192,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => NoteTargetWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
noteTargets: Relation<NoteTargetWorkspaceEntity[]>;
@WorkspaceRelation({

View file

@ -18,6 +18,7 @@ import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-enti
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -174,6 +175,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
// Relations
@ -214,6 +216,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => TaskTargetWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
taskTargets: Relation<TaskTargetWorkspaceEntity[]>;
@WorkspaceRelation({
@ -225,6 +228,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
inverseSideTarget: () => NoteTargetWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsFieldUIReadOnly()
noteTargets: Relation<NoteTargetWorkspaceEntity[]>;
@WorkspaceRelation({

View file

@ -13,6 +13,7 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsFieldUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-field-ui-readonly.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@ -125,6 +126,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
icon: 'IconCreativeCommonsSa',
description: msg`The creator of the record`,
})
@WorkspaceIsFieldUIReadOnly()
createdBy: ActorMetadata;
@WorkspaceRelation({

View file

@ -15,6 +15,7 @@ import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsObjectUIReadOnly } from 'src/engine/twenty-orm/decorators/workspace-is-object-ui-readonly.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
@ -80,6 +81,7 @@ export const SEARCH_FIELDS_FOR_WORKFLOW_RUNS: FieldTypeAndNameMetadata[] = [
icon: STANDARD_OBJECT_ICONS.workflowRun,
})
@WorkspaceIsNotAuditLogged()
@WorkspaceIsObjectUIReadOnly()
export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,

Some files were not shown because too many files have changed in this diff Show more