From da5ae341096f518fbb68cd1a16309b6bce0ff0be Mon Sep 17 00:00:00 2001
From: Marie <51697796+ijreilly@users.noreply.github.com>
Date: Wed, 18 Jun 2025 17:12:58 +0200
Subject: [PATCH] [permissions] Filter tabs + registered actions according to
permissions (#12657)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Note and task tabs in side panel should only show if user has reading
permission on them.
"Go to companies", "Go to workflows", etc. in command menu should only
show is user has reading permission on related objects.
**How to test**
Assign a user with a custom role that has **no** read permissions on
notes/tasks/workflows/companies/opportunities/people (no need to test
them all but at least one between note and tasks; workflows; one between
companies/opportunities/people). Check that you don't see the related
tab / action.
---------
Co-authored-by: Charles Bochet
---
.../constants/DefaultRecordActionsConfig.tsx | 65 ++++++++++++++-----
.../types/ShouldBeRegisteredFunctionParams.ts | 3 +
.../actions/utils/getActionConfig.ts | 17 +++--
.../action-menu/hooks/useRegisteredActions.ts | 4 +-
.../useShouldActionBeRegisteredParams.ts | 18 ++++-
.../states/objectPermissionsFamilySelector.ts | 37 +++++++++++
.../__stories__/CommandMenu.stories.tsx | 31 +++++++++
.../record-show/constants/BaseRecordLayout.ts | 4 ++
.../hooks/useRecordShowContainerTabs.ts | 26 ++++++--
.../layout/tab-list/types/RecordLayoutTab.ts | 1 +
.../tab-list/types/TabVisibilityConfig.ts | 1 +
.../src/testing/mock-data/users.ts | 34 ++++++++--
12 files changed, 206 insertions(+), 35 deletions(-)
create mode 100644 packages/twenty-front/src/modules/auth/states/objectPermissionsFamilySelector.ts
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx
index c0ed019ca4..f3f5b31b98 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig.tsx
@@ -410,7 +410,13 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
Icon: IconSettingsAutomation,
accent: 'default',
isPinned: false,
- shouldBeRegistered: ({ objectMetadataItem, viewType, isWorkflowEnabled }) =>
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ viewType,
+ isWorkflowEnabled,
+ getTargetObjectReadPermission,
+ }) =>
+ getTargetObjectReadPermission(CoreObjectNameSingular.Workflow) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Workflow ||
viewType === ActionViewType.SHOW_PAGE) &&
isWorkflowEnabled,
@@ -443,9 +449,14 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_BULK_SELECTION,
ActionViewType.SHOW_PAGE,
],
- shouldBeRegistered: ({ objectMetadataItem, viewType }) =>
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Person ||
- viewType === ActionViewType.SHOW_PAGE,
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ viewType,
+ getTargetObjectReadPermission,
+ }) =>
+ getTargetObjectReadPermission(CoreObjectNameSingular.Person) === true &&
+ (objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Person ||
+ viewType === ActionViewType.SHOW_PAGE),
component: (
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Company ||
- viewType === ActionViewType.SHOW_PAGE,
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ viewType,
+ getTargetObjectReadPermission,
+ }) =>
+ getTargetObjectReadPermission(CoreObjectNameSingular.Company) === true &&
+ (objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Company ||
+ viewType === ActionViewType.SHOW_PAGE),
component: (
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Opportunity ||
- viewType === ActionViewType.SHOW_PAGE,
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ viewType,
+ getTargetObjectReadPermission,
+ }) =>
+ getTargetObjectReadPermission(CoreObjectNameSingular.Opportunity) ===
+ true &&
+ (objectMetadataItem?.nameSingular !==
+ CoreObjectNameSingular.Opportunity ||
+ viewType === ActionViewType.SHOW_PAGE),
component: (
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Task ||
- viewType === ActionViewType.SHOW_PAGE,
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ viewType,
+ getTargetObjectReadPermission,
+ }) =>
+ getTargetObjectReadPermission(CoreObjectNameSingular.Task) === true &&
+ (objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Task ||
+ viewType === ActionViewType.SHOW_PAGE),
component: (
- objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Note ||
- viewType === ActionViewType.SHOW_PAGE,
+ shouldBeRegistered: ({
+ objectMetadataItem,
+ viewType,
+ getTargetObjectReadPermission,
+ }) =>
+ getTargetObjectReadPermission(CoreObjectNameSingular.Note) === true &&
+ (objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Note ||
+ viewType === ActionViewType.SHOW_PAGE),
component: (
boolean;
};
diff --git a/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts b/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts
index 145ef834e6..30dc8644ca 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts
+++ b/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts
@@ -7,20 +7,25 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
-export const getActionConfig = (
- objectMetadataItem?: ObjectMetadataItem,
-): Record => {
+export const getActionConfig = ({
+ objectMetadataItem,
+}: {
+ objectMetadataItem?: ObjectMetadataItem;
+}): Record => {
if (!isDefined(objectMetadataItem)) {
return {};
}
switch (objectMetadataItem.nameSingular) {
- case CoreObjectNameSingular.Workflow:
+ case CoreObjectNameSingular.Workflow: {
return WORKFLOW_ACTIONS_CONFIG;
- case CoreObjectNameSingular.WorkflowVersion:
+ }
+ case CoreObjectNameSingular.WorkflowVersion: {
return WORKFLOW_VERSIONS_ACTIONS_CONFIG;
- case CoreObjectNameSingular.WorkflowRun:
+ }
+ case CoreObjectNameSingular.WorkflowRun: {
return WORKFLOW_RUNS_ACTIONS_CONFIG;
+ }
default:
return DEFAULT_RECORD_ACTIONS_CONFIG;
}
diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useRegisteredActions.ts b/packages/twenty-front/src/modules/action-menu/hooks/useRegisteredActions.ts
index add0872a59..658b04d954 100644
--- a/packages/twenty-front/src/modules/action-menu/hooks/useRegisteredActions.ts
+++ b/packages/twenty-front/src/modules/action-menu/hooks/useRegisteredActions.ts
@@ -26,7 +26,9 @@ export const useRegisteredActions = (
contextStoreTargetedRecordsRule,
);
- const recordActionConfig = getActionConfig(objectMetadataItem);
+ const recordActionConfig = getActionConfig({
+ objectMetadataItem,
+ });
const recordAgnosticActionConfig = RECORD_AGNOSTIC_ACTIONS_CONFIG;
diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts
index 9e96479071..484af15674 100644
--- a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts
+++ b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts
@@ -1,6 +1,7 @@
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
+import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@@ -14,7 +15,7 @@ import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-t
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
-import { useRecoilValue } from 'recoil';
+import { useRecoilCallback, useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useShouldActionBeRegisteredParams = ({
@@ -77,6 +78,20 @@ export const useShouldActionBeRegisteredParams = ({
contextStoreTargetedRecordsRule,
);
+ const getObjectReadPermission = useRecoilCallback(
+ ({ snapshot }) =>
+ (objectMetadataNameSingular: string) => {
+ return snapshot
+ .getLoadable(
+ objectPermissionsFamilySelector({
+ objectNameSingular: objectMetadataNameSingular,
+ }),
+ )
+ .getValue().canRead;
+ },
+ [],
+ );
+
return {
objectMetadataItem,
isFavorite,
@@ -89,5 +104,6 @@ export const useShouldActionBeRegisteredParams = ({
isWorkflowEnabled,
numberOfSelectedRecords,
viewType: viewType ?? undefined,
+ getTargetObjectReadPermission: getObjectReadPermission,
};
};
diff --git a/packages/twenty-front/src/modules/auth/states/objectPermissionsFamilySelector.ts b/packages/twenty-front/src/modules/auth/states/objectPermissionsFamilySelector.ts
new file mode 100644
index 0000000000..8a292a7d8d
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/states/objectPermissionsFamilySelector.ts
@@ -0,0 +1,37 @@
+import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { selectorFamily } from 'recoil';
+
+export const objectPermissionsFamilySelector = selectorFamily<
+ {
+ canRead: boolean;
+ },
+ { objectNameSingular: string }
+>({
+ key: 'objectPermissionsFamilySelector',
+ get:
+ ({ objectNameSingular }) =>
+ ({ get }) => {
+ const currentUserWorkspace = get(currentUserWorkspaceState);
+ const objectMetadataItems = get(objectMetadataItemsState);
+
+ const objectMetadataItem = objectMetadataItems.find(
+ (item) => item.nameSingular === objectNameSingular,
+ );
+
+ if (!objectMetadataItem) {
+ return {
+ canRead: false,
+ canUpdate: false,
+ };
+ }
+
+ const objectPermissions = currentUserWorkspace?.objectPermissions?.find(
+ (permission) => permission.objectMetadataId === objectMetadataItem.id,
+ );
+
+ return {
+ canRead: objectPermissions?.canReadObjectRecords ?? false,
+ };
+ },
+});
diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
index 5abf9fc146..6e284e892b 100644
--- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
@@ -10,11 +10,14 @@ import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import {
mockCurrentWorkspace,
+ mockedLimitedPermissionsUserData,
+ mockedUserData,
mockedWorkspaceMemberData,
} from '~/testing/mock-data/users';
import { sleep } from '~/utils/sleep';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
@@ -72,6 +75,9 @@ const meta: Meta = {
I18nFrontDecorator,
(Story) => {
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
+ const setCurrentUserWorkspace = useSetRecoilState(
+ currentUserWorkspaceState,
+ );
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
@@ -84,6 +90,8 @@ const meta: Meta = {
setCurrentWorkspace(mockCurrentWorkspace);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
+ setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
+
setIsCommandMenuOpened(true);
setCommandMenuNavigationStack([
{
@@ -122,6 +130,29 @@ export const DefaultWithoutSearch: Story = {
},
};
+export const LimitedPermissions: Story = {
+ play: async () => {
+ const canvas = within(document.body);
+ await expect(canvas.findByText('Go to Opportunities')).rejects.toThrow();
+ await expect(canvas.findByText('Go to Tasks')).rejects.toThrow();
+ expect(await canvas.findByText('Go to People')).toBeVisible();
+ expect(await canvas.findByText('Go to Settings')).toBeVisible();
+ expect(await canvas.findByText('Go to Notes')).toBeVisible();
+ },
+ decorators: [
+ (Story) => {
+ const setCurrentUserWorkspace = useSetRecoilState(
+ currentUserWorkspaceState,
+ );
+ setCurrentUserWorkspace(
+ mockedLimitedPermissionsUserData.currentUserWorkspace,
+ );
+
+ return ;
+ },
+ ],
+};
+
export const MatchingNavigate: Story = {
play: async () => {
const canvas = within(document.body);
diff --git a/packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts b/packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts
index 3892da6835..7091608a8c 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts
+++ b/packages/twenty-front/src/modules/object-record/record-show/constants/BaseRecordLayout.ts
@@ -44,6 +44,7 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
Icon: IconCheckbox,
position: 300,
cards: [{ type: CardType.TaskCard }],
+ targetObjectNameSingular: CoreObjectNameSingular.Task,
hide: {
ifMobile: false,
ifDesktop: false,
@@ -51,6 +52,7 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [CoreObjectNameSingular.Task],
ifRelationsMissing: ['taskTargets'],
+ ifNoReadPermission: true,
},
},
notes: {
@@ -58,6 +60,7 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
Icon: IconNotes,
position: 400,
cards: [{ type: CardType.NoteCard }],
+ targetObjectNameSingular: CoreObjectNameSingular.Note,
hide: {
ifMobile: false,
ifDesktop: false,
@@ -65,6 +68,7 @@ export const BASE_RECORD_LAYOUT: RecordLayout = {
ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [CoreObjectNameSingular.Note],
ifRelationsMissing: ['noteTargets'],
+ ifNoReadPermission: true,
},
},
files: {
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts
index 8cbf1e8ca3..e78755e5ac 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts
@@ -2,6 +2,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { BASE_RECORD_LAYOUT } from '@/object-record/record-show/constants/BaseRecordLayout';
import { CardType } from '@/object-record/record-show/types/CardType';
import { RecordLayout } from '@/object-record/record-show/types/RecordLayout';
@@ -10,6 +11,7 @@ import { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
import {
IconCalendarEvent,
IconHome,
@@ -30,6 +32,7 @@ export const useRecordShowContainerTabs = (
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
+ const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
// Object-specific layouts that override or extend the base layout
const OBJECT_SPECIFIC_LAYOUTS: Partial<
@@ -212,17 +215,19 @@ export const useRecordShowContainerTabs = (
[],
);
+ const baseRecordLayout = BASE_RECORD_LAYOUT;
+
// Merge base layout with object-specific layout
const recordLayout: RecordLayout = useMemo(() => {
return {
- ...BASE_RECORD_LAYOUT,
+ ...baseRecordLayout,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular] || {}),
tabs: {
- ...BASE_RECORD_LAYOUT.tabs,
+ ...baseRecordLayout.tabs,
...(OBJECT_SPECIFIC_LAYOUTS[targetObjectNameSingular]?.tabs || {}),
},
};
- }, [OBJECT_SPECIFIC_LAYOUTS, targetObjectNameSingular]);
+ }, [OBJECT_SPECIFIC_LAYOUTS, baseRecordLayout, targetObjectNameSingular]);
return {
layout: recordLayout,
@@ -232,7 +237,7 @@ export const useRecordShowContainerTabs = (
entry[1] !== null && entry[1] !== undefined,
)
.sort(([, a], [, b]) => a.position - b.position)
- .map(([key, { title, Icon, hide, cards }]) => {
+ .map(([key, { title, Icon, hide, cards, targetObjectNameSingular }]) => {
// Special handling for fields tab
if (key === 'fields') {
return {
@@ -257,6 +262,16 @@ export const useRecordShowContainerTabs = (
);
});
+ const targetObjectMetadataId = objectMetadataItems.find(
+ (item) => item.nameSingular === targetObjectNameSingular,
+ )?.id;
+
+ const permissionHide =
+ hide.ifNoReadPermission &&
+ isDefined(targetObjectNameSingular) &&
+ !objectPermissionsByObjectMetadataId[targetObjectMetadataId]
+ ?.canReadObjectRecords;
+
const requiredObjectsInactive =
hide.ifRequiredObjectsInactive.length > 0 &&
!hide.ifRequiredObjectsInactive.every((obj) =>
@@ -286,7 +301,8 @@ export const useRecordShowContainerTabs = (
baseHide ||
featureNotEnabled ||
requiredObjectsInactive ||
- relationsDontExist,
+ relationsDontExist ||
+ permissionHide,
};
})
// When isInRightDrawer === true, we merge first and second tab into first tab
diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts
index c4cdc28318..d23e71e3b4 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts
+++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/RecordLayoutTab.ts
@@ -8,4 +8,5 @@ export type RecordLayoutTab = {
Icon: IconComponent;
hide: TabVisibilityConfig;
cards: LayoutCard[];
+ targetObjectNameSingular?: string;
};
diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts b/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts
index cc7fd55d3a..713d4b8528 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts
+++ b/packages/twenty-front/src/modules/ui/layout/tab-list/types/TabVisibilityConfig.ts
@@ -8,4 +8,5 @@ export type TabVisibilityConfig = {
ifFeaturesDisabled: FeatureFlagKey[];
ifRequiredObjectsInactive: CoreObjectNameSingular[];
ifRelationsMissing: string[];
+ ifNoReadPermission?: boolean;
};
diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts
index 6f174bceea..e6da563ae8 100644
--- a/packages/twenty-front/src/testing/mock-data/users.ts
+++ b/packages/twenty-front/src/testing/mock-data/users.ts
@@ -12,6 +12,7 @@ import {
WorkspaceMemberDateFormatEnum,
WorkspaceMemberTimeFormatEnum,
} from '~/generated/graphql';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
type MockedUser = Pick<
User,
@@ -128,12 +129,13 @@ export const mockedUserData: MockedUser = {
currentWorkspace: mockCurrentWorkspace,
currentUserWorkspace: {
settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS],
- objectPermissions: [
- {
- objectMetadataId: '4a45f524-b8cb-40e8-8450-28e402b442cf',
- canReadObjectRecords: true,
- },
- ],
+ objectPermissions: generatedMockObjectMetadataItems.map((item) => ({
+ objectMetadataId: item.id,
+ canReadObjectRecords: true,
+ canUpdateObjectRecords: true,
+ canSoftDeleteObjectRecords: true,
+ canDestroyObjectRecords: true,
+ })),
},
locale: 'en',
workspaces: [{ workspace: mockCurrentWorkspace }],
@@ -146,6 +148,26 @@ export const mockedUserData: MockedUser = {
userVars: {},
};
+export const mockedLimitedPermissionsUserData: MockedUser = {
+ ...mockedUserData,
+ currentUserWorkspace: {
+ ...mockedUserData.currentUserWorkspace,
+ objectPermissions: generatedMockObjectMetadataItems
+ .filter(
+ (objectMetadata) =>
+ objectMetadata.nameSingular !== 'task' &&
+ objectMetadata.nameSingular !== 'opportunity',
+ )
+ .map((item) => ({
+ objectMetadataId: item.id,
+ canReadObjectRecords: true,
+ canUpdateObjectRecords: true,
+ canSoftDeleteObjectRecords: true,
+ canDestroyObjectRecords: true,
+ })),
+ },
+};
+
export const mockedOnboardingUserData = (
onboardingStatus?: OnboardingStatus,
) => {