diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index d707329401..af7d56eea4 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1417,6 +1417,7 @@ export enum FeatureFlagKey { IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', + IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED', IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED', IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED', IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index c2dd291d6b..25aad9513c 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1384,6 +1384,7 @@ export enum FeatureFlagKey { IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', + IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED', IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED', IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED', IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED', 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 323b8592d5..01589207c7 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 @@ -21,6 +21,10 @@ import { NavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/re import { NavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/components/NavigateToPreviousRecordSingleRecordAction'; import { RemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/components/RemoveFromFavoritesSingleRecordAction'; import { RestoreSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/components/RestoreSingleRecordAction'; +import { CancelRecordPageLayoutSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction'; +import { EditRecordPageLayoutSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction'; +import { SaveRecordPageLayoutSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction'; +import { RecordPageLayoutSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/types/RecordPageLayoutSingleRecordActionKeys'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig'; import { ActionScope } from '@/action-menu/actions/types/ActionScope'; @@ -36,9 +40,11 @@ import { AppPath, SettingsPath } from 'twenty-shared/types'; import { IconArrowMerge, IconBuildingSkyscraper, + IconCancel, IconCheckbox, IconChevronDown, IconChevronUp, + IconDeviceFloppy, IconEdit, IconEyeOff, IconFileExport, @@ -47,6 +53,7 @@ import { IconHeartOff, IconLayout, IconLayoutDashboard, + IconPencil, IconPlus, IconRefresh, IconRotate2, @@ -60,11 +67,13 @@ import { import { isDefined } from 'twenty-shared/utils'; import { PermissionFlagType } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< | NoSelectionRecordActionKeys | SingleRecordActionKeys - | MultipleRecordsActionKeys, + | MultipleRecordsActionKeys + | RecordPageLayoutSingleRecordActionKeys, ActionConfig > = { [SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: { @@ -741,4 +750,87 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record< ), hotKeys: ['G', 'N'], }, + + [RecordPageLayoutSingleRecordActionKeys.EDIT_RECORD_PAGE_LAYOUT]: { + key: RecordPageLayoutSingleRecordActionKeys.EDIT_RECORD_PAGE_LAYOUT, + label: msg`Edit Page Layout`, + shortLabel: msg`Edit Layout`, + isPinned: true, + position: 30, + Icon: IconPencil, + type: ActionType.Standard, + scope: ActionScope.RecordSelection, + requiredPermissionFlag: PermissionFlagType.LAYOUTS, + shouldBeRegistered: ({ + selectedRecord, + objectPermissions, + objectMetadataItem, + isFeatureFlagEnabled, + }) => + isFeatureFlagEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED, + ) && + isDefined(selectedRecord) && + !selectedRecord?.isRemote && + !isDefined(selectedRecord?.deletedAt) && + objectPermissions.canUpdateObjectRecords && + objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard, + availableOn: [ActionViewType.SHOW_PAGE], + component: , + }, + [RecordPageLayoutSingleRecordActionKeys.SAVE_RECORD_PAGE_LAYOUT]: { + key: RecordPageLayoutSingleRecordActionKeys.SAVE_RECORD_PAGE_LAYOUT, + label: msg`Save Page Layout`, + shortLabel: msg`Save`, + isPinned: true, + isPrimaryCTA: true, + position: 31, + Icon: IconDeviceFloppy, + type: ActionType.Standard, + scope: ActionScope.RecordSelection, + requiredPermissionFlag: PermissionFlagType.LAYOUTS, + shouldBeRegistered: ({ + selectedRecord, + objectPermissions, + objectMetadataItem, + isFeatureFlagEnabled, + }) => + isFeatureFlagEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED, + ) && + isDefined(selectedRecord) && + !selectedRecord?.isRemote && + !isDefined(selectedRecord?.deletedAt) && + objectPermissions.canUpdateObjectRecords && + objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard, + availableOn: [ActionViewType.PAGE_EDIT_MODE], + component: , + }, + [RecordPageLayoutSingleRecordActionKeys.CANCEL_RECORD_PAGE_LAYOUT_EDITION]: { + key: RecordPageLayoutSingleRecordActionKeys.CANCEL_RECORD_PAGE_LAYOUT_EDITION, + label: msg`Cancel Edition`, + shortLabel: msg`Cancel`, + isPinned: true, + position: 32, + Icon: IconCancel, + type: ActionType.Standard, + scope: ActionScope.RecordSelection, + requiredPermissionFlag: PermissionFlagType.LAYOUTS, + shouldBeRegistered: ({ + selectedRecord, + objectPermissions, + objectMetadataItem, + isFeatureFlagEnabled, + }) => + isFeatureFlagEnabled( + FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED, + ) && + isDefined(selectedRecord) && + !selectedRecord?.isRemote && + !isDefined(selectedRecord?.deletedAt) && + objectPermissions.canUpdateObjectRecords && + objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard, + availableOn: [ActionViewType.PAGE_EDIT_MODE], + component: , + }, }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx new file mode 100644 index 0000000000..460194ba54 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction.tsx @@ -0,0 +1,35 @@ +import { Action } from '@/action-menu/actions/components/Action'; +import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; +import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout'; +import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; + +export const CancelRecordPageLayoutSingleRecordAction = () => { + const recordId = useSelectedRecordIdOrThrow(); + + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ + id: recordId, + targetObjectNameSingular: objectMetadataItem.nameSingular, + }); + + const { closeCommandMenu } = useCommandMenu(); + + const { setIsPageLayoutInEditMode } = + useSetIsPageLayoutInEditMode(pageLayoutId); + + const { resetDraftPageLayoutToPersistedPageLayout } = + useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId); + + const handleClick = () => { + closeCommandMenu(); + + resetDraftPageLayoutToPersistedPageLayout(); + setIsPageLayoutInEditMode(false); + }; + + return ; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx new file mode 100644 index 0000000000..4ee4230a44 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction.tsx @@ -0,0 +1,29 @@ +import { Action } from '@/action-menu/actions/components/Action'; +import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; +import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; +import { useResetLocationHash } from 'twenty-ui/utilities'; + +export const EditRecordPageLayoutSingleRecordAction = () => { + const recordId = useSelectedRecordIdOrThrow(); + + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ + id: recordId, + targetObjectNameSingular: objectMetadataItem.nameSingular, + }); + + const { setIsPageLayoutInEditMode } = + useSetIsPageLayoutInEditMode(pageLayoutId); + + const { resetLocationHash } = useResetLocationHash(); + + const handleClick = () => { + setIsPageLayoutInEditMode(true); + resetLocationHash(); + }; + + return ; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx new file mode 100644 index 0000000000..37cf36cb4e --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction.tsx @@ -0,0 +1,36 @@ +import { Action } from '@/action-menu/actions/components/Action'; +import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; +import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout'; +import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode'; + +export const SaveRecordPageLayoutSingleRecordAction = () => { + const recordId = useSelectedRecordIdOrThrow(); + + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ + id: recordId, + targetObjectNameSingular: objectMetadataItem.nameSingular, + }); + + const { savePageLayout } = useSavePageLayout(pageLayoutId); + + const { setIsPageLayoutInEditMode } = + useSetIsPageLayoutInEditMode(pageLayoutId); + + const { closeCommandMenu } = useCommandMenu(); + + const handleClick = async () => { + const result = await savePageLayout(); + + if (result.status === 'successful') { + closeCommandMenu(); + setIsPageLayoutInEditMode(false); + } + }; + + return ; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/types/RecordPageLayoutSingleRecordActionKeys.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/types/RecordPageLayoutSingleRecordActionKeys.ts new file mode 100644 index 0000000000..e8dbde1654 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/record-page-layout-actions/types/RecordPageLayoutSingleRecordActionKeys.ts @@ -0,0 +1,5 @@ +export enum RecordPageLayoutSingleRecordActionKeys { + EDIT_RECORD_PAGE_LAYOUT = 'edit-record-page-layout-single-record', + SAVE_RECORD_PAGE_LAYOUT = 'save-record-page-layout-single-record', + CANCEL_RECORD_PAGE_LAYOUT_EDITION = 'cancel-record-page-layout-edition-single-record', +} diff --git a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts index f349eaba83..3f9cb59a9d 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/types/ShouldBeRegisteredFunctionParams.ts @@ -4,6 +4,7 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { type WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; import { type ObjectPermissions } from 'twenty-shared/types'; +import { type FeatureFlagKey } from '~/generated/graphql'; export type ShouldBeRegisteredFunctionParams = { objectMetadataItem?: ObjectMetadataItem; @@ -28,4 +29,5 @@ export type ShouldBeRegisteredFunctionParams = { objectMetadataItemNameSingular: string, ) => boolean; forceRegisteredActionsByKey: Record; + isFeatureFlagEnabled: (featureFlagKey: FeatureFlagKey) => boolean; }; 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 319ad57faa..34e9966240 100644 --- a/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useShouldActionBeRegisteredParams.ts @@ -2,6 +2,7 @@ import { forceRegisteredActionsByKeyState } from '@/action-menu/actions/states/f import { type ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams'; import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; @@ -135,6 +136,16 @@ export const useShouldActionBeRegisteredParams = ({ forceRegisteredActionsByKeyState, ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const isFeatureFlagEnabled = (featureFlagKey: FeatureFlagKey) => { + const featureFlag = currentWorkspace?.featureFlags?.find( + (flag) => flag.key === featureFlagKey, + ); + + return featureFlag?.value === true; + }; + return { objectMetadataItem, isFavorite, @@ -150,5 +161,6 @@ export const useShouldActionBeRegisteredParams = ({ getTargetObjectReadPermission: getObjectReadPermission, getTargetObjectWritePermission: getObjectWritePermission, forceRegisteredActionsByKey, + isFeatureFlagEnabled, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx index 70eddeb81e..17609accf8 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx @@ -3,12 +3,13 @@ import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLay import { PageLayoutVerticalListEditor } from '@/page-layout/components/PageLayoutVerticalListEditor'; import { PageLayoutVerticalListViewer } from '@/page-layout/components/PageLayoutVerticalListViewer'; import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext'; +import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow'; import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets'; import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { FeatureFlagKey, PageLayoutType } from '~/generated/graphql'; export const PageLayoutContent = () => { const isRecordPageEnabled = useIsFeatureEnabled( @@ -27,6 +28,11 @@ export const PageLayoutContent = () => { const { layoutMode } = usePageLayoutContentContext(); + const { currentPageLayout } = useCurrentPageLayoutOrThrow(); + + const isRecordPageLayout = + currentPageLayout.type === PageLayoutType.RECORD_PAGE; + const isCanvasLayout = isRecordPageEnabled && layoutMode === 'canvas'; const isVerticalList = isRecordPageEnabled && layoutMode === 'vertical-list'; @@ -39,6 +45,7 @@ export const PageLayoutContent = () => { ) : ( diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx index 9eb8cbbc57..f7a32ab378 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx @@ -1,7 +1,10 @@ -import { usePageLayoutShouldUseWhiteBackground } from '@/page-layout/hooks/usePageLayoutShouldUseWhiteBackground'; +import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant'; import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState'; +import { type PageLayoutVerticalListViewerVariant } from '@/page-layout/types/PageLayoutVerticalListViewerVariant'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; +import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab'; +import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; import { @@ -11,8 +14,10 @@ import { type DropResult, } from '@hello-pangea/dnd'; import { useId } from 'react'; +import { useIsMobile } from 'twenty-ui/utilities'; const StyledVerticalListContainer = styled.div<{ + variant: PageLayoutVerticalListViewerVariant; shouldUseWhiteBackground: boolean; }>` background: ${({ theme, shouldUseWhiteBackground }) => @@ -21,7 +26,9 @@ const StyledVerticalListContainer = styled.div<{ : theme.background.secondary}; display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(2)}; + gap: ${({ theme }) => theme.spacing(4)}; + padding: ${({ theme, variant }) => + variant === 'side-column' ? theme.spacing(1) : theme.spacing(2)}; `; const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>` @@ -34,15 +41,25 @@ const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>` type PageLayoutVerticalListEditorProps = { widgets: PageLayoutWidget[]; onReorder: (result: DropResult) => void; + isReorderEnabled?: boolean; }; export const PageLayoutVerticalListEditor = ({ widgets, onReorder, + isReorderEnabled = true, }: PageLayoutVerticalListEditorProps) => { const droppableId = `page-layout-vertical-list-${useId()}`; - const { shouldUseWhiteBackground } = usePageLayoutShouldUseWhiteBackground(); + const { isInRightDrawer } = useLayoutRenderingContext(); + const isMobile = useIsMobile(); + const { isInPinnedTab } = useIsInPinnedTab(); + + const variant = getPageLayoutVerticalListViewerVariant({ + isInPinnedTab, + isMobile, + isInRightDrawer, + }); const setDraggingWidgetId = useSetRecoilComponentState( pageLayoutDraggingWidgetIdComponentState, @@ -62,12 +79,18 @@ export const PageLayoutVerticalListEditor = ({ {(provided) => ( {widgets.map((widget, index) => ( - + {(provided, snapshot) => ( ` background: ${({ theme, shouldUseWhiteBackground }) => shouldUseWhiteBackground @@ -21,6 +20,8 @@ const StyledVerticalListContainer = styled.div<{ flex-direction: column; gap: ${({ theme, variant }) => variant === 'side-column' ? 0 : theme.spacing(2)}; + padding: ${({ theme, variant }) => + variant === 'side-column' ? 0 : theme.spacing(2)}; `; type PageLayoutVerticalListViewerProps = { @@ -30,7 +31,6 @@ type PageLayoutVerticalListViewerProps = { export const PageLayoutVerticalListViewer = ({ widgets, }: PageLayoutVerticalListViewerProps) => { - const { shouldUseWhiteBackground } = usePageLayoutShouldUseWhiteBackground(); const { isInRightDrawer } = useLayoutRenderingContext(); const isMobile = useIsMobile(); const { isInPinnedTab } = useIsInPinnedTab(); @@ -43,8 +43,8 @@ export const PageLayoutVerticalListViewer = ({ return ( {widgets.map((widget) => (
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutShouldUseWhiteBackground.ts b/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutShouldUseWhiteBackground.ts deleted file mode 100644 index 968a596ef6..0000000000 --- a/packages/twenty-front/src/modules/page-layout/hooks/usePageLayoutShouldUseWhiteBackground.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { PageLayoutType } from '~/generated/graphql'; - -export const usePageLayoutShouldUseWhiteBackground = () => { - const isMobile = useIsMobile(); - const { isInRightDrawer, layoutType } = useLayoutRenderingContext(); - - const shouldUseWhiteBackground = - layoutType === PageLayoutType.RECORD_PAGE && (isMobile || isInRightDrawer); - - return { shouldUseWhiteBackground }; -}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts index 3f4891affa..6c3f41faf4 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutId.ts @@ -1,30 +1,7 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId'; -import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId'; -import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId'; -import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId'; -import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId'; -import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId'; -import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId'; -import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId'; -import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId'; +import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId'; import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; -import { isDefined } from 'twenty-shared/utils'; - -const OBJECT_NAME_TO_DEFAULT_LAYOUT_ID: Record = { - [CoreObjectNameSingular.Company]: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.Person]: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.Opportunity]: - DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.Note]: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.Task]: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.Workflow]: DEFAULT_WORKFLOW_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.WorkflowVersion]: - DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID, - [CoreObjectNameSingular.WorkflowRun]: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID, -}; export const useRecordPageLayoutId = ({ id, @@ -37,23 +14,12 @@ export const useRecordPageLayoutId = ({ }, ); - if (!isDefined(record)) { - return { - pageLayoutId: null, - }; - } - - if (isDefined(record.pageLayoutId)) { - return { - pageLayoutId: record.pageLayoutId, - }; - } - - const defaultLayoutId = - OBJECT_NAME_TO_DEFAULT_LAYOUT_ID[targetObjectNameSingular] ?? - DEFAULT_RECORD_PAGE_LAYOUT_ID; + const pageLayoutId = getRecordPageLayoutId({ + record, + targetObjectNameSingular, + }); return { - pageLayoutId: defaultLayoutId, + pageLayoutId, }; }; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts new file mode 100644 index 0000000000..373a6f3e33 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow.ts @@ -0,0 +1,29 @@ +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId'; +import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; + +export const useRecordPageLayoutIdFromRecordStoreOrThrow = ({ + id, + targetObjectNameSingular, +}: TargetRecordIdentifier) => { + const record = useRecoilValue(recordStoreFamilyState(id)); + + if (!isDefined(record)) { + throw new Error(`Record with id ${id} not found in record store`); + } + + const pageLayoutId = getRecordPageLayoutId({ + record, + targetObjectNameSingular, + }); + + if (!isDefined(pageLayoutId)) { + throw new Error(`Page layout id not found for record with id ${id}`); + } + + return { + pageLayoutId, + }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/getRecordPageLayoutId.ts b/packages/twenty-front/src/modules/page-layout/utils/getRecordPageLayoutId.ts new file mode 100644 index 0000000000..3305bd32d6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/utils/getRecordPageLayoutId.ts @@ -0,0 +1,47 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId'; +import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId'; +import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId'; +import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId'; +import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId'; +import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId'; +import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId'; +import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId'; +import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId'; +import { isDefined } from 'twenty-shared/utils'; + +const OBJECT_NAME_TO_DEFAULT_LAYOUT_ID: Record = { + [CoreObjectNameSingular.Company]: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Person]: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Opportunity]: + DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Note]: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Task]: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.Workflow]: DEFAULT_WORKFLOW_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.WorkflowVersion]: + DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID, + [CoreObjectNameSingular.WorkflowRun]: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID, +}; + +export const getRecordPageLayoutId = ({ + record, + targetObjectNameSingular, +}: { + record: ObjectRecord | null | undefined; + targetObjectNameSingular: string; +}): string | null => { + if (!isDefined(record)) { + return null; + } + + if (isDefined(record.pageLayoutId)) { + return record.pageLayoutId; + } + + const defaultLayoutId = + OBJECT_NAME_TO_DEFAULT_LAYOUT_ID[targetObjectNameSingular] ?? + DEFAULT_RECORD_PAGE_LAYOUT_ID; + + return defaultLayoutId; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index 394933f9c1..d1dc206ce3 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -29,7 +29,7 @@ import styled from '@emotion/styled'; import { type MouseEvent } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { IconLock } from 'twenty-ui/display'; -import { WidgetType } from '~/generated/graphql'; +import { PageLayoutType, WidgetType } from '~/generated/graphql'; const StyledNoAccessContainer = styled.div` align-items: center; @@ -79,6 +79,9 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { const isLastWidget = useIsCurrentWidgetLastOfTab(widget.id); + const isReorderEnabled = + currentPageLayout.type !== PageLayoutType.RECORD_PAGE; + // TODO: when we have more widgets without headers, we should use a more generic approach to hide the header // each widget type could have metadata (e.g., hasHeader: boolean or headerMode: 'always' | 'editOnly' | 'never') const isRichTextWidget = widget.type === WidgetType.STANDALONE_RICH_TEXT; @@ -144,6 +147,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { variant={variant} isInEditMode={isPageLayoutInEditMode} isResizing={isResizing} + isReorderEnabled={isReorderEnabled} title={widget.title} onRemove={handleRemove} actions={actions} @@ -158,7 +162,11 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => { /> )} - + {hasAccess ? ( ` box-sizing: border-box; display: grid; @@ -20,14 +21,17 @@ const StyledWidgetCardContent = styled.div<{ } `} - ${({ theme, variant }) => { + ${({ theme, variant, isEditable }) => { if (variant === 'dashboard') { return css` padding: ${theme.spacing(2)}; `; } - if (variant === 'record-page') { + if ( + variant === 'record-page' || + (variant === 'side-column' && isEditable) + ) { return css` border: 1px solid ${theme.border.color.medium}; border-radius: ${theme.border.radius.md}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx index 19f8059ab7..8310a9df88 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx @@ -25,6 +25,7 @@ export type WidgetCardHeaderProps = { actions?: WidgetAction[]; className?: string; isResizing?: boolean; + isReorderEnabled?: boolean; }; const StyledWidgetCardHeader = styled.div` @@ -78,6 +79,7 @@ export const WidgetCardHeader = ({ isEmpty = false, isInEditMode = false, isResizing = false, + isReorderEnabled = true, title, onRemove, forbiddenDisplay, @@ -94,7 +96,7 @@ export const WidgetCardHeader = ({ return ( - {!isEmpty && isInEditMode && ( + {!isEmpty && isInEditMode && isReorderEnabled && ( e.stopPropagation()} diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 6f6120d24d..823687ddbe 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -5,6 +5,7 @@ export enum FeatureFlagKey { IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED', IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED', IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED', + IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED', diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 1ce5d393ab..743d3b383a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -226,6 +226,7 @@ describe('WorkspaceEntityManager', () => { IS_NAVIGATION_MENU_ITEM_ENABLED: false, IS_FILES_FIELD_ENABLED: false, IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false, + IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false, }, userWorkspaceRoleMap: {}, eventEmitterService: {