diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useEnterLayoutCustomizationMode.test.tsx b/packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useEnterLayoutCustomizationMode.test.tsx
new file mode 100644
index 0000000000..183cd75d4b
--- /dev/null
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/__tests__/useEnterLayoutCustomizationMode.test.tsx
@@ -0,0 +1,70 @@
+import { renderHook } from '@testing-library/react';
+import { createStore, Provider as JotaiProvider } from 'jotai';
+import { type ReactNode } from 'react';
+
+import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
+import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
+import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
+import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
+
+jest.mock('@/settings/roles/hooks/useHasPermissionFlag');
+jest.mock('@/side-panel/hooks/useNavigateSidePanel', () => ({
+ useNavigateSidePanel: () => ({
+ navigateSidePanel: jest.fn(),
+ }),
+}));
+jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({
+ useSnackBar: () => ({
+ enqueueWarningSnackBar: jest.fn(),
+ }),
+}));
+
+const mockUseHasPermissionFlag = useHasPermissionFlag as jest.Mock;
+
+const getWrapper =
+ (store = createStore()) =>
+ ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+
+describe('useEnterLayoutCustomizationMode', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return false and not enable customization mode when user lacks LAYOUTS permission', () => {
+ mockUseHasPermissionFlag.mockReturnValue(false);
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ const { result } = renderHook(() => useEnterLayoutCustomizationMode(), {
+ wrapper,
+ });
+
+ const success = result.current.enterLayoutCustomizationMode();
+
+ expect(success).toBe(false);
+ expect(store.get(isLayoutCustomizationModeEnabledState.atom)).toBe(false);
+ });
+
+ it('should return true and enable customization mode when user has LAYOUTS permission', () => {
+ mockUseHasPermissionFlag.mockReturnValue(true);
+ const store = createStore();
+ const wrapper = getWrapper(store);
+
+ store.set(metadataStoreState.atomFamily('navigationMenuItems'), {
+ current: [],
+ draft: [],
+ status: 'up-to-date',
+ });
+
+ const { result } = renderHook(() => useEnterLayoutCustomizationMode(), {
+ wrapper,
+ });
+
+ const success = result.current.enterLayoutCustomizationMode();
+
+ expect(success).toBe(true);
+ expect(store.get(isLayoutCustomizationModeEnabledState.atom)).toBe(true);
+ });
+});
diff --git a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
index 57e992c867..8764fdd52d 100644
--- a/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
+++ b/packages/twenty-front/src/modules/layout-customization/hooks/useEnterLayoutCustomizationMode.ts
@@ -15,17 +15,25 @@ import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/state
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { isDashboardInEditModeComponentState } from '@/page-layout/states/isDashboardInEditModeComponentState';
+import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { PermissionFlagType } from '~/generated-metadata/graphql';
+
export const useEnterLayoutCustomizationMode = () => {
const store = useStore();
const { navigateSidePanel } = useNavigateSidePanel();
const { enqueueWarningSnackBar } = useSnackBar();
+ const hasLayoutsPermission = useHasPermissionFlag(PermissionFlagType.LAYOUTS);
const enterLayoutCustomizationMode = useCallback((): boolean => {
+ if (!hasLayoutsPermission) {
+ return false;
+ }
+
const isLayoutCustomizationModeAlreadyEnabled = store.get(
isLayoutCustomizationModeEnabledState.atom,
);
@@ -86,7 +94,7 @@ export const useEnterLayoutCustomizationMode = () => {
}
return true;
- }, [enqueueWarningSnackBar, navigateSidePanel, store]);
+ }, [enqueueWarningSnackBar, hasLayoutsPermission, navigateSidePanel, store]);
return { enterLayoutCustomizationMode };
};
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
index ef885a3617..da87542a5a 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/display/sections/workspace/components/WorkspaceSection.tsx
@@ -2,6 +2,7 @@ import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useNavigate } from 'react-router-dom';
+import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import {
IconColumnInsertRight,
IconLink,
@@ -27,15 +28,17 @@ import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sectio
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
+import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
+import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
-import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
-import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
+
+import { PermissionFlagType } from '~/generated-metadata/graphql';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -52,6 +55,7 @@ export const WorkspaceSection = () => {
lastVisitedViewPerObjectMetadataItemState,
);
const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
+ const hasLayoutsPermission = useHasPermissionFlag(PermissionFlagType.LAYOUTS);
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
);
@@ -169,7 +173,9 @@ export const WorkspaceSection = () => {
objectMetadataItem: EnrichedObjectMetadataItem,
navigationMenuItemId: string,
) => {
- enterLayoutCustomizationMode();
+ if (!enterLayoutCustomizationMode()) {
+ return;
+ }
setSelectedNavigationMenuItemIdInEditMode(navigationMenuItemId);
openNavigationMenuItemInSidePanel({
pageTitle: objectMetadataItem.labelSingular,
@@ -201,14 +207,16 @@ export const WorkspaceSection = () => {
onClick={handleAddMenuItem}
/>
) : (
-
-
-
+ hasLayoutsPermission && (
+
+
+
+ )
)}
}
diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx
index c9bcc72819..01146d2391 100644
--- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectLayout.tsx
@@ -5,10 +5,10 @@ import { useLingui } from '@lingui/react/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconLayoutDashboard, IconReload } from 'twenty-ui/icon';
-import { H2Title } from 'twenty-ui/typography';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+import { H2Title } from 'twenty-ui/typography';
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
@@ -16,9 +16,12 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useResetPageLayoutToDefault } from '@/page-layout/hooks/useResetPageLayoutToDefault';
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
import { SettingsCard } from '@/settings/components/SettingsCard';
+import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
+
+import { PermissionFlagType } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledContentContainer = styled.div`
@@ -38,6 +41,7 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
const { t } = useLingui();
const navigateApp = useNavigateApp();
const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
+ const hasLayoutsPermission = useHasPermissionFlag(PermissionFlagType.LAYOUTS);
const { openModal } = useModal();
const { resetPageLayoutToDefault } = useResetPageLayoutToDefault();
@@ -55,7 +59,7 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
const firstRecord = records[0];
const handleCustomizeRecordPage = () => {
- if (!isDefined(firstRecord)) {
+ if (!hasLayoutsPermission || !isDefined(firstRecord)) {
return;
}
@@ -74,7 +78,7 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
};
const handleConfirmReset = async () => {
- if (!isDefined(pageLayout)) {
+ if (!hasLayoutsPermission || !isDefined(pageLayout)) {
return;
}
@@ -94,7 +98,7 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
title={t`Customize record page`}
Icon={}
onClick={handleCustomizeRecordPage}
- disabled={!isDefined(firstRecord)}
+ disabled={!hasLayoutsPermission || !isDefined(firstRecord)}
/>
@@ -108,7 +112,7 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
size="small"
Icon={IconReload}
onClick={handleResetPageLayout}
- disabled={!isDefined(pageLayout)}
+ disabled={!hasLayoutsPermission || !isDefined(pageLayout)}
/>