diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx index 750d3ab418..6bcd4c8194 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx @@ -1,6 +1,6 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; @@ -9,7 +9,7 @@ import { GraphQLView } from '@/views/types/GraphQLView'; import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; export const ObjectMetadataNavItems = () => { - const { activeObjectMetadataItems } = useObjectMetadataItemForSettings(); + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); const navigate = useNavigate(); const { getIcon } = useIcons(); const currentPath = useLocation().pathname; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useObjectMetadataItemForSettings.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems.ts similarity index 100% rename from packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useObjectMetadataItemForSettings.ts rename to packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems.ts diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx similarity index 86% rename from packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx rename to packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx index 8739fd03c8..f745f25767 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx @@ -7,8 +7,8 @@ import { query, responseData, variables, -} from '@/object-metadata/hooks/__mocks__/useObjectMetadataItemForSettings'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +} from '@/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; @@ -36,14 +36,14 @@ const Wrapper = ({ children }: { children: ReactNode }) => ( const mockObjectMetadataItems = getObjectMetadataItemsMock(); -describe('useObjectMetadataItemForSettings', () => { +describe('useFilteredObjectMetadataItems', () => { it('should findActiveObjectMetadataItemBySlug', async () => { const { result } = renderHook( () => { const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); - return useObjectMetadataItemForSettings(); + return useFilteredObjectMetadataItems(); }, { wrapper: Wrapper, @@ -63,7 +63,7 @@ describe('useObjectMetadataItemForSettings', () => { const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); - return useObjectMetadataItemForSettings(); + return useFilteredObjectMetadataItems(); }, { wrapper: Wrapper, @@ -85,7 +85,7 @@ describe('useObjectMetadataItemForSettings', () => { const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); - return useObjectMetadataItemForSettings(); + return useFilteredObjectMetadataItems(); }, { wrapper: Wrapper, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts similarity index 95% rename from packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts rename to packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts index eb8bfe651f..2ac05fa4f9 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFilteredObjectMetadataItems.ts @@ -4,7 +4,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { getObjectSlug } from '../utils/getObjectSlug'; -export const useObjectMetadataItemForSettings = () => { +export const useFilteredObjectMetadataItems = () => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const activeObjectMetadataItems = objectMetadataItems.filter( diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx index 5876df99ea..76ecf2a83c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; @@ -58,7 +58,7 @@ export const SettingsObjectFieldRelationForm = ({ }: SettingsObjectFieldRelationFormProps) => { const { getIcon } = useIcons(); const { objectMetadataItems, findObjectMetadataItemById } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); const selectedObjectMetadataItem = (values.objectMetadataId diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index f506605559..0cf85a8157 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { SettingsDataModelDefaultValueForm } from '@/settings/data-model/components/SettingsDataModelDefaultValue'; import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; @@ -79,7 +79,7 @@ export const SettingsDataModelFieldSettingsFormCard = ({ relationFieldMetadataItem, values, }: SettingsDataModelFieldSettingsFormCardProps) => { - const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); + const { findObjectMetadataItemById } = useFilteredObjectMetadataItems(); if (!previewableTypes.includes(fieldMetadataItem.type)) return null; diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx index fd24ab76f1..4ee949ff97 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPageHeader.tsx @@ -1,7 +1,7 @@ import { useParams } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { PageAddButton } from '@/ui/layout/page/PageAddButton'; @@ -20,7 +20,10 @@ export const RecordIndexPageHeader = ({ const objectNamePlural = useParams().objectNamePlural ?? ''; const { findObjectMetadataItemByNamePlural } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); + + const objectMetadataItem = + findObjectMetadataItemByNamePlural(objectNamePlural); const { getIcon } = useIcons(); const Icon = getIcon( @@ -29,12 +32,13 @@ export const RecordIndexPageHeader = ({ const recordIndexViewType = useRecoilValue(recordIndexViewTypeState); + const canAddRecord = + recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote; + return ( - {recordIndexViewType === ViewType.Table && ( - - )} + {canAddRecord && } ); }; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx index 6c8f23d1b0..713f92f325 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx @@ -5,7 +5,7 @@ import { IconPlus, IconSettings } from 'twenty-ui'; import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; @@ -45,7 +45,7 @@ export const SettingsObjectDetail = () => { const { objectSlug = '' } = useParams(); const { findActiveObjectMetadataItemBySlug } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const activeObjectMetadataItem = diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx index 4e7f77ace0..417b4e8f0f 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx @@ -6,7 +6,7 @@ import pick from 'lodash.pick'; import { IconArchive, IconSettings } from 'twenty-ui'; import { z } from 'zod'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; @@ -44,7 +44,7 @@ export const SettingsObjectEdit = () => { const { objectSlug = '' } = useParams(); const { findActiveObjectMetadataItemBySlug } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const activeObjectMetadataItem = diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 1a8da0b0f3..16b2c17562 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -5,8 +5,8 @@ import { isNonEmptyString } from '@sniptt/guards'; import { IconArchive, IconSettings } from 'twenty-ui'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; @@ -52,7 +52,7 @@ export const SettingsObjectFieldEdit = () => { const { objectSlug = '', fieldSlug = '' } = useParams(); const { findActiveObjectMetadataItemBySlug } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx index 58e96e7976..4e5cf0b74c 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { IconMinus, IconPlus, IconSettings } from 'twenty-ui'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; -import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; @@ -39,7 +39,7 @@ export const SettingsObjectNewFieldStep1 = () => { const { objectSlug = '' } = useParams(); const { findActiveObjectMetadataItemBySlug } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); @@ -114,11 +114,13 @@ export const SettingsObjectNewFieldStep1 = () => { { children: 'New Field' }, ]} /> - navigate(`/settings/objects/${objectSlug}`)} - onSave={handleSave} - /> + {!activeObjectMetadataItem.isRemote && ( + navigate(`/settings/objects/${objectSlug}`)} + onSave={handleSave} + /> + )} { findActiveObjectMetadataItemBySlug, findObjectMetadataItemById, findObjectMetadataItemByNamePlural, - } = useObjectMetadataItemForSettings(); + } = useFilteredObjectMetadataItems(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); @@ -300,11 +300,13 @@ export const SettingsObjectNewFieldStep2 = () => { { children: 'New Field' }, ]} /> - navigate(`/settings/objects/${objectSlug}`)} - onSave={handleSave} - /> + {!activeObjectMetadataItem.isRemote && ( + navigate(`/settings/objects/${objectSlug}`)} + onSave={handleSave} + /> + )} { const navigate = useNavigate(); const { activeObjectMetadataItems, inactiveObjectMetadataItems } = - useObjectMetadataItemForSettings(); + useFilteredObjectMetadataItems(); const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index 2c0dc67938..712372e441 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -46,6 +46,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm import { NotFoundError } from 'src/engine/utils/graphql-errors.util'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface'; import { @@ -217,6 +218,9 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise { const { workspaceId, userId, objectMetadataItem } = options; + + assertMutationNotOnRemoteObject(objectMetadataItem); + const computedArgs = await this.queryRunnerArgsFactory.create( args, options, @@ -273,6 +277,8 @@ export class WorkspaceQueryRunnerService { ): Promise { const { workspaceId, userId, objectMetadataItem } = options; + assertMutationNotOnRemoteObject(objectMetadataItem); + const existingRecord = await this.findOne( { filter: { id: { eq: args.id } } } as FindOneResolverArgs, options, @@ -318,6 +324,9 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise { const { workspaceId, objectMetadataItem } = options; + + assertMutationNotOnRemoteObject(objectMetadataItem); + const maximumRecordAffected = this.environmentService.get( 'MUTATION_MAXIMUM_RECORD_AFFECTED', ); @@ -359,6 +368,9 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise { const { workspaceId, userId, objectMetadataItem } = options; + + assertMutationNotOnRemoteObject(objectMetadataItem); + const maximumRecordAffected = this.environmentService.get( 'MUTATION_MAXIMUM_RECORD_AFFECTED', ); @@ -403,6 +415,9 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise { const { workspaceId, userId, objectMetadataItem } = options; + + assertMutationNotOnRemoteObject(objectMetadataItem); + const query = await this.workspaceQueryBuilderFactory.deleteOne( args, options, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index f090e1d646..1f5aff80be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -38,6 +38,7 @@ import { } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { computeCustomName } from 'src/engine/utils/compute-custom-name.util'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { FieldMetadataEntity, @@ -94,6 +95,10 @@ export class FieldMetadataService extends TypeOrmQueryService { + if (objectMetadataItem.isRemote) { + throw new Error('Remote objects are read-only'); + } +};