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');
+ }
+};