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