diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/RecordPageLayoutActionsConfig.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/RecordPageLayoutActionsConfig.tsx
new file mode 100644
index 00000000000..3ca7ba3ba34
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/constants/RecordPageLayoutActionsConfig.tsx
@@ -0,0 +1,114 @@
+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 { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
+import { inheritActionsFromDefaultConfig } from '@/action-menu/actions/record-actions/utils/inheritActionsFromDefaultConfig';
+import { ActionScope } from '@/action-menu/actions/types/ActionScope';
+import { ActionType } from '@/action-menu/actions/types/ActionType';
+import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
+import { PageLayoutSingleRecordActionKeys } from '@/page-layout/actions/PageLayoutSingleRecordActionKeys';
+import { msg } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+import { IconDeviceFloppy, IconPencil, IconX } from 'twenty-ui/display';
+
+export const RECORD_PAGE_LAYOUT_ACTIONS_CONFIG =
+ inheritActionsFromDefaultConfig({
+ config: {
+ [PageLayoutSingleRecordActionKeys.EDIT_LAYOUT]: {
+ key: PageLayoutSingleRecordActionKeys.EDIT_LAYOUT,
+ label: msg`Edit Layout`,
+ shortLabel: msg`Edit`,
+ isPinned: true,
+ position: 0,
+ Icon: IconPencil,
+ type: ActionType.Standard,
+ scope: ActionScope.RecordSelection,
+ shouldBeRegistered: ({ selectedRecord }) =>
+ isDefined(selectedRecord) &&
+ !selectedRecord?.isRemote &&
+ !isDefined(selectedRecord?.deletedAt),
+ // TODO: Once backend is ready, uncomment the line below
+ // isDefined(selectedRecord?.pageLayoutId),
+ availableOn: [ActionViewType.SHOW_PAGE],
+ component: ,
+ },
+ [PageLayoutSingleRecordActionKeys.SAVE_LAYOUT]: {
+ key: PageLayoutSingleRecordActionKeys.SAVE_LAYOUT,
+ label: msg`Save Layout`,
+ shortLabel: msg`Save`,
+ isPinned: true,
+ position: 1,
+ Icon: IconDeviceFloppy,
+ type: ActionType.Standard,
+ scope: ActionScope.RecordSelection,
+ shouldBeRegistered: ({ selectedRecord }) =>
+ isDefined(selectedRecord) &&
+ !selectedRecord?.isRemote &&
+ !isDefined(selectedRecord?.deletedAt),
+ // TODO: Once backend is ready, uncomment the line below
+ // isDefined(selectedRecord?.pageLayoutId),
+ availableOn: [ActionViewType.SHOW_PAGE],
+ component: ,
+ },
+ [PageLayoutSingleRecordActionKeys.CANCEL_LAYOUT_EDITION]: {
+ key: PageLayoutSingleRecordActionKeys.CANCEL_LAYOUT_EDITION,
+ label: msg`Cancel Edition`,
+ shortLabel: msg`Cancel`,
+ isPinned: true,
+ position: 2,
+ Icon: IconX,
+ type: ActionType.Standard,
+ scope: ActionScope.RecordSelection,
+ shouldBeRegistered: ({ selectedRecord }) =>
+ isDefined(selectedRecord) &&
+ !selectedRecord?.isRemote &&
+ !isDefined(selectedRecord?.deletedAt),
+ // TODO: Once backend is ready, uncomment the line below
+ // isDefined(selectedRecord?.pageLayoutId),
+ availableOn: [ActionViewType.SHOW_PAGE],
+ component: ,
+ },
+ },
+ actionKeys: [
+ SingleRecordActionKeys.ADD_TO_FAVORITES,
+ SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
+ SingleRecordActionKeys.DELETE,
+ SingleRecordActionKeys.DESTROY,
+ SingleRecordActionKeys.RESTORE,
+ SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW,
+ SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
+ SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
+ ],
+ propertiesToOverwrite: {
+ [SingleRecordActionKeys.ADD_TO_FAVORITES]: {
+ position: 3,
+ },
+ [SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
+ position: 4,
+ },
+ [SingleRecordActionKeys.DELETE]: {
+ position: 5,
+ label: msg`Delete record`,
+ },
+ [SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: {
+ position: 6,
+ label: msg`Export record`,
+ },
+ [SingleRecordActionKeys.DESTROY]: {
+ position: 7,
+ label: msg`Permanently destroy record`,
+ },
+ [SingleRecordActionKeys.RESTORE]: {
+ position: 8,
+ label: msg`Restore record`,
+ },
+ [SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD]: {
+ position: 9,
+ label: msg`Navigate to previous record`,
+ },
+ [SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
+ position: 10,
+ label: msg`Navigate to next record`,
+ },
+ },
+ });
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 00000000000..2ac54ffb4e5
--- /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,37 @@
+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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { DEFAULT_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPageLayoutId';
+import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useRecoilValue } from 'recoil';
+
+export const CancelRecordPageLayoutSingleRecordAction = () => {
+ const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
+
+ const recordId = useSelectedRecordIdOrThrow();
+
+ const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
+
+ // For COMPANY objects, use DEFAULT_PAGE_LAYOUT_ID
+ // For other objects, use the record's pageLayoutId (when backend is ready)
+ const pageLayoutId =
+ objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
+ ? DEFAULT_PAGE_LAYOUT_ID
+ : selectedRecord?.pageLayoutId;
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { resetDraftPageLayoutToPersistedPageLayout } =
+ useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
+
+ const handleClick = () => {
+ 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 00000000000..74e37c064c7
--- /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,32 @@
+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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { DEFAULT_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPageLayoutId';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useRecoilValue } from 'recoil';
+
+export const EditRecordPageLayoutSingleRecordAction = () => {
+ const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
+
+ const recordId = useSelectedRecordIdOrThrow();
+
+ const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
+
+ // For COMPANY objects, use DEFAULT_PAGE_LAYOUT_ID
+ // For other objects, use the record's pageLayoutId (when backend is ready)
+ const pageLayoutId =
+ objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
+ ? DEFAULT_PAGE_LAYOUT_ID
+ : selectedRecord?.pageLayoutId;
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const handleClick = () => {
+ setIsPageLayoutInEditMode(true);
+ };
+
+ 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 00000000000..54571245f36
--- /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 { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { DEFAULT_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPageLayoutId';
+import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useRecoilValue } from 'recoil';
+
+export const SaveRecordPageLayoutSingleRecordAction = () => {
+ const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
+
+ const recordId = useSelectedRecordIdOrThrow();
+
+ const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
+
+ // For COMPANY objects, use DEFAULT_PAGE_LAYOUT_ID
+ // For other objects, use the record's pageLayoutId (when backend is ready)
+ const pageLayoutId =
+ objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
+ ? DEFAULT_PAGE_LAYOUT_ID
+ : selectedRecord?.pageLayoutId;
+
+ const { savePageLayout } = useSavePageLayout(pageLayoutId);
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const handleClick = async () => {
+ await savePageLayout();
+ setIsPageLayoutInEditMode(false);
+ };
+
+ return ;
+};
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 dca13c49b4c..31ea3ebe50f 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
@@ -1,5 +1,6 @@
import { DASHBOARD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/DashboardActionsConfig';
import { DEFAULT_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig';
+import { RECORD_PAGE_LAYOUT_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/RecordPageLayoutActionsConfig';
import { WORKFLOW_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowActionsConfig';
import { WORKFLOW_RUNS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig';
import { WORKFLOW_VERSIONS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowVersionsActionsConfig';
@@ -10,8 +11,10 @@ import { isDefined } from 'twenty-shared/utils';
export const getActionConfig = ({
objectMetadataItem,
+ isRecordPageLayoutEnabled,
}: {
objectMetadataItem?: ObjectMetadataItem;
+ isRecordPageLayoutEnabled: boolean;
}): Record => {
if (!isDefined(objectMetadataItem)) {
return {};
@@ -30,6 +33,13 @@ export const getActionConfig = ({
case CoreObjectNameSingular.WorkflowRun: {
return WORKFLOW_RUNS_ACTIONS_CONFIG;
}
+ case CoreObjectNameSingular.Company: {
+ if (isRecordPageLayoutEnabled) {
+ return RECORD_PAGE_LAYOUT_ACTIONS_CONFIG;
+ }
+
+ return DEFAULT_RECORD_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 854ad450504..da16ffc8689 100644
--- a/packages/twenty-front/src/modules/action-menu/hooks/useRegisteredActions.ts
+++ b/packages/twenty-front/src/modules/action-menu/hooks/useRegisteredActions.ts
@@ -8,8 +8,10 @@ import { contextStoreCurrentViewTypeComponentState } from '@/context-store/state
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
+import { FeatureFlagKey } from '~/generated/graphql';
export const useRegisteredActions = (
shouldBeRegisteredParams: ShouldBeRegisteredFunctionParams,
@@ -27,6 +29,10 @@ export const useRegisteredActions = (
contextStoreCurrentViewTypeComponentState,
);
+ const isRecordPageLayoutEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
+ );
+
const viewType = getActionViewType(
contextStoreCurrentViewType,
contextStoreTargetedRecordsRule,
@@ -34,6 +40,7 @@ export const useRegisteredActions = (
const recordActionConfig = getActionConfig({
objectMetadataItem,
+ isRecordPageLayoutEnabled,
});
const relatedRecordActionConfig = useRelatedRecordActions({
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx
new file mode 100644
index 00000000000..ca40888029a
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutContent.tsx
@@ -0,0 +1,64 @@
+import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
+import { PageLayoutVerticalListEditor } from '@/page-layout/components/PageLayoutVerticalListEditor';
+import { PageLayoutVerticalListViewer } from '@/page-layout/components/PageLayoutVerticalListViewer';
+import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
+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 styled from '@emotion/styled';
+import { isDefined } from 'twenty-shared/utils';
+import { FeatureFlagKey } from '~/generated/graphql';
+
+const StyledContainer = styled.div`
+ background: ${({ theme }) => theme.background.primary};
+ box-sizing: border-box;
+ flex: 1;
+ min-height: 100%;
+ position: relative;
+ padding: ${({ theme }) => theme.spacing(2)};
+ width: 100%;
+`;
+
+type PageLayoutContentProps = {
+ tabId: string;
+};
+
+export const PageLayoutContent = ({ tabId }: PageLayoutContentProps) => {
+ const isRecordPageEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
+ );
+
+ const isPageLayoutInEditMode = useRecoilComponentValue(
+ isPageLayoutInEditModeComponentState,
+ );
+
+ const { currentPageLayout } = useCurrentPageLayout();
+ const { reorderWidgets } = useReorderPageLayoutWidgets(tabId);
+
+ const activeTab = currentPageLayout?.tabs.find((tab) => tab.id === tabId);
+
+ if (!isDefined(currentPageLayout) || !isDefined(activeTab)) {
+ return null;
+ }
+
+ const isVerticalList =
+ isRecordPageEnabled && activeTab.layoutMode === 'vertical-list';
+
+ if (isVerticalList) {
+ return (
+
+ {isPageLayoutInEditMode ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutLeftPanel.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutLeftPanel.tsx
index 79a8367420a..bf44f8c08bf 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutLeftPanel.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutLeftPanel.tsx
@@ -1,5 +1,5 @@
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
-import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
+import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
@@ -29,7 +29,7 @@ export const PageLayoutLeftPanel = ({
isInRightDrawer={isInRightDrawer}
/>
-
+
);
};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
index 08a4ec2b132..426dc5d8228 100644
--- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutRendererContent.tsx
@@ -1,4 +1,4 @@
-import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
+import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
import { PageLayoutTabHeader } from '@/page-layout/components/PageLayoutTabHeader';
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
@@ -93,7 +93,7 @@ export const PageLayoutRendererContent = () => {
defaultEnableXScroll={false}
>
{isDefined(activeTabId) && (
-
+
)}
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx
new file mode 100644
index 00000000000..0172f8d49a4
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx
@@ -0,0 +1,68 @@
+import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
+import styled from '@emotion/styled';
+import {
+ DragDropContext,
+ Draggable,
+ Droppable,
+ type DropResult,
+} from '@hello-pangea/dnd';
+import { useId } from 'react';
+import { type PageLayoutWidget } from '~/generated/graphql';
+
+const StyledVerticalListContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>`
+ background: ${({ theme, isDragging }) =>
+ isDragging ? theme.background.transparent.light : 'transparent'};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ transition: background 0.1s ease;
+`;
+
+type PageLayoutVerticalListEditorProps = {
+ widgets: PageLayoutWidget[];
+ onReorder: (result: DropResult) => void;
+};
+
+export const PageLayoutVerticalListEditor = ({
+ widgets,
+ onReorder,
+}: PageLayoutVerticalListEditorProps) => {
+ const droppableId = `page-layout-vertical-list-${useId()}`;
+
+ return (
+
+
+ {(provided) => (
+
+ {widgets.map((widget, index) => (
+
+ {(provided, snapshot) => (
+
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */}
+
+
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx
new file mode 100644
index 00000000000..b33c1986969
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx
@@ -0,0 +1,27 @@
+import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
+import styled from '@emotion/styled';
+import { type PageLayoutWidget } from '~/generated/graphql';
+
+const StyledVerticalListContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+
+type PageLayoutVerticalListViewerProps = {
+ widgets: PageLayoutWidget[];
+};
+
+export const PageLayoutVerticalListViewer = ({
+ widgets,
+}: PageLayoutVerticalListViewerProps) => {
+ return (
+
+ {widgets.map((widget) => (
+
+
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultPageLayout.ts
index 865a3d48843..a5167557d7e 100644
--- a/packages/twenty-front/src/modules/page-layout/constants/DefaultPageLayout.ts
+++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultPageLayout.ts
@@ -115,6 +115,25 @@ export const DEFAULT_PAGE_LAYOUT: PageLayout = {
updatedAt: new Date().toISOString(),
deletedAt: null,
},
+ {
+ __typename: 'PageLayoutWidget',
+ id: 'default-widget-tasks-2',
+ pageLayoutTabId: 'default-tab-tasks',
+ title: 'Tasks',
+ type: WidgetType.TASKS,
+ objectMetadataId: null,
+ gridPosition: {
+ __typename: 'GridPosition',
+ row: 0,
+ column: 0,
+ rowSpan: 6,
+ columnSpan: 12,
+ },
+ configuration: null,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ deletedAt: null,
+ },
],
},
// Notes tab (position 400)
diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutWidgets.ts b/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutWidgets.ts
new file mode 100644
index 00000000000..81cadf5fa38
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/hooks/useReorderPageLayoutWidgets.ts
@@ -0,0 +1,48 @@
+import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
+import { type DropResult } from '@hello-pangea/dnd';
+import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+
+export const useReorderPageLayoutWidgets = (
+ tabId: string,
+ pageLayoutIdFromProps?: string,
+) => {
+ const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
+ PageLayoutComponentInstanceContext,
+ pageLayoutIdFromProps,
+ );
+
+ const pageLayoutDraftState = useRecoilComponentCallbackState(
+ pageLayoutDraftComponentState,
+ pageLayoutId,
+ );
+
+ const reorderWidgets = useRecoilCallback(
+ ({ set }) =>
+ (result: DropResult) => {
+ if (!result.destination) return;
+
+ set(pageLayoutDraftState, (prev) => {
+ const tab = prev.tabs.find((t) => t.id === tabId);
+ if (!isDefined(tab)) return prev;
+
+ const newWidgets = Array.from(tab.widgets ?? []);
+ const [removed] = newWidgets.splice(result.source.index, 1);
+ newWidgets.splice(result.destination!.index, 0, removed);
+
+ return {
+ ...prev,
+ tabs: prev.tabs.map((t) =>
+ t.id === tabId ? { ...t, widgets: newWidgets } : t,
+ ),
+ };
+ });
+ },
+ [tabId, pageLayoutDraftState],
+ );
+
+ return { reorderWidgets };
+};