Implement basic edition for record page layouts (#15237)

1. Add actions for record page layouts to side panel
2. Support drag-and-drop of widgets


https://github.com/user-attachments/assets/c4cef7c5-08a8-4999-b8a5-e1831cc66709
This commit is contained in:
Baptiste Devessier
2025-10-22 10:48:09 +02:00
committed by GitHub
parent c2a477a9d6
commit c6addc2bb4
13 changed files with 466 additions and 4 deletions
@@ -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 (
<StyledContainer>
{isPageLayoutInEditMode ? (
<PageLayoutVerticalListEditor
widgets={activeTab.widgets}
onReorder={reorderWidgets}
/>
) : (
<PageLayoutVerticalListViewer widgets={activeTab.widgets} />
)}
</StyledContainer>
);
}
return <PageLayoutGridLayout tabId={tabId} />;
};
@@ -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}
/>
<PageLayoutGridLayout tabId={pinnedLeftTabId} />
<PageLayoutContent tabId={pinnedLeftTabId} />
</ShowPageLeftContainer>
);
};
@@ -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) && (
<PageLayoutGridLayout tabId={activeTabId} />
<PageLayoutContent tabId={activeTabId} />
)}
</StyledScrollWrapper>
</StyledTabsAndDashboardContainer>
@@ -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 (
<DragDropContext onDragEnd={onReorder}>
<Droppable droppableId={droppableId}>
{(provided) => (
<StyledVerticalListContainer
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{widgets.map((widget, index) => (
<Draggable key={widget.id} draggableId={widget.id} index={index}>
{(provided, snapshot) => (
<StyledDraggableWrapper
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.draggableProps}
isDragging={snapshot.isDragging}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...provided.dragHandleProps}>
<WidgetRenderer widget={widget} />
</div>
</StyledDraggableWrapper>
)}
</Draggable>
))}
{provided.placeholder}
</StyledVerticalListContainer>
)}
</Droppable>
</DragDropContext>
);
};
@@ -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 (
<StyledVerticalListContainer>
{widgets.map((widget) => (
<div key={widget.id}>
<WidgetRenderer widget={widget} />
</div>
))}
</StyledVerticalListContainer>
);
};