Support workflows in record page layouts (#15471)

https://github.com/user-attachments/assets/275a02a3-7054-45d1-9423-75bb5223a8ba
This commit is contained in:
Baptiste Devessier
2025-10-31 18:45:58 +01:00
committed by GitHub
parent 09c704c8bd
commit 5f0d24798a
34 changed files with 607 additions and 164 deletions
@@ -0,0 +1,33 @@
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
const StyledCanvasContainer = styled.div`
display: grid;
height: 100%;
`;
type PageLayoutCanvasViewerProps = {
widgets: PageLayoutWidget[];
};
export const PageLayoutCanvasViewer = ({
widgets,
}: PageLayoutCanvasViewerProps) => {
const widget = widgets.at(0);
if (!isDefined(widget)) {
throw new Error('No widget found in canvas layout');
}
return (
<StyledCanvasContainer>
<WidgetRenderer
widget={widget}
pageLayoutType={PageLayoutType.RECORD_PAGE}
layoutMode="canvas"
/>
</StyledCanvasContainer>
);
};
@@ -1,3 +1,4 @@
import { PageLayoutCanvasViewer } from '@/page-layout/components/PageLayoutCanvasViewer';
import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
import { PageLayoutVerticalListEditor } from '@/page-layout/components/PageLayoutVerticalListEditor';
import { PageLayoutVerticalListViewer } from '@/page-layout/components/PageLayoutVerticalListViewer';
@@ -42,9 +43,15 @@ export const PageLayoutContent = ({ tabId }: PageLayoutContentProps) => {
return null;
}
const isCanvasLayout =
isRecordPageEnabled && activeTab.layoutMode === 'canvas';
const isVerticalList =
isRecordPageEnabled && activeTab.layoutMode === 'vertical-list';
if (isCanvasLayout) {
return <PageLayoutCanvasViewer widgets={activeTab.widgets} />;
}
if (isVerticalList) {
return (
<StyledContainer>
@@ -16,7 +16,6 @@ import { WidgetPlaceholder } from '@/page-layout/widgets/components/WidgetPlaceh
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { useRef } from 'react';
import {
@@ -25,7 +24,6 @@ import {
type ResponsiveProps,
} from 'react-grid-layout';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
const StyledGridContainer = styled.div`
background: ${({ theme }) => theme.background.primary};
@@ -61,21 +59,11 @@ const ResponsiveGridLayout = WidthProvider(
Responsive,
) as React.ComponentType<ExtendedResponsiveProps>;
const StyledVerticalListContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
type PageLayoutGridLayoutProps = {
tabId: string;
};
export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
const isRecordPageEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
);
const setPageLayoutCurrentBreakpoint = useSetRecoilComponentState(
pageLayoutCurrentBreakpointComponentState,
);
@@ -113,18 +101,6 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
? EMPTY_LAYOUT
: (pageLayoutCurrentLayouts[tabId] ?? EMPTY_LAYOUT);
const Widgets = isLayoutEmpty ? (
<div key="empty-placeholder" data-select-disable="true">
<WidgetPlaceholder />
</div>
) : (
activeTabWidgets?.map((widget) => (
<div key={widget.id} data-select-disable="true">
<WidgetRenderer widget={widget} />
</div>
))
);
return (
<>
<StyledGridContainer ref={gridContainerRef}>
@@ -137,47 +113,53 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
</>
)}
{isRecordPageEnabled &&
!isPageLayoutInEditMode &&
activeTab.layoutMode === 'vertical-list' ? (
<StyledVerticalListContainer>{Widgets}</StyledVerticalListContainer>
) : (
<ResponsiveGridLayout
className="layout"
layouts={layouts}
breakpoints={PAGE_LAYOUT_CONFIG.breakpoints}
cols={PAGE_LAYOUT_CONFIG.columns}
rowHeight={55}
maxCols={12}
containerPadding={[0, 0]}
margin={[8, 8]}
isDraggable={isPageLayoutInEditMode}
isResizable={isPageLayoutInEditMode}
draggableHandle=".drag-handle"
compactType="vertical"
preventCollision={false}
resizeHandle={
isPageLayoutInEditMode ? (
<PageLayoutGridResizeHandle />
) : undefined
}
resizeHandles={['se']}
onDragStart={(_layout, _oldItem, newItem) => {
setDraggingWidgetId(newItem.i);
}}
onDragStop={() => {
setDraggingWidgetId(null);
}}
onLayoutChange={handleLayoutChange}
onBreakpointChange={(newBreakpoint) =>
setPageLayoutCurrentBreakpoint(
newBreakpoint as PageLayoutBreakpoint,
)
}
>
{Widgets}
</ResponsiveGridLayout>
)}
<ResponsiveGridLayout
className="layout"
layouts={layouts}
breakpoints={PAGE_LAYOUT_CONFIG.breakpoints}
cols={PAGE_LAYOUT_CONFIG.columns}
rowHeight={55}
maxCols={12}
containerPadding={[0, 0]}
margin={[8, 8]}
isDraggable={isPageLayoutInEditMode}
isResizable={isPageLayoutInEditMode}
draggableHandle=".drag-handle"
compactType="vertical"
preventCollision={false}
resizeHandle={
isPageLayoutInEditMode ? <PageLayoutGridResizeHandle /> : undefined
}
resizeHandles={['se']}
onDragStart={(_layout, _oldItem, newItem) => {
setDraggingWidgetId(newItem.i);
}}
onDragStop={() => {
setDraggingWidgetId(null);
}}
onLayoutChange={handleLayoutChange}
onBreakpointChange={(newBreakpoint) =>
setPageLayoutCurrentBreakpoint(
newBreakpoint as PageLayoutBreakpoint,
)
}
>
{isLayoutEmpty ? (
<div key="empty-placeholder" data-select-disable="true">
<WidgetPlaceholder />
</div>
) : (
activeTabWidgets?.map((widget) => (
<div key={widget.id} data-select-disable="true">
<WidgetRenderer
widget={widget}
pageLayoutType={currentPageLayout.type}
layoutMode="grid"
/>
</div>
))
)}
</ResponsiveGridLayout>
</StyledGridContainer>
</>
);
@@ -11,6 +11,12 @@ import { DEFAULT_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultRecor
import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT } from '@/page-layout/constants/DefaultTaskRecordPageLayout';
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
import { DEFAULT_WORKFLOW_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowPageLayout';
import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowRunPageLayout';
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT } from '@/page-layout/constants/DefaultWorkflowVersionPageLayout';
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
@@ -37,6 +43,12 @@ const getDefaultLayoutById = (layoutId: string): PageLayout => {
return DEFAULT_NOTE_RECORD_PAGE_LAYOUT;
case DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID:
return DEFAULT_TASK_RECORD_PAGE_LAYOUT;
case DEFAULT_WORKFLOW_PAGE_LAYOUT_ID:
return DEFAULT_WORKFLOW_PAGE_LAYOUT;
case DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID:
return DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT;
case DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID:
return DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT;
case DEFAULT_RECORD_PAGE_LAYOUT_ID:
default:
return DEFAULT_RECORD_PAGE_LAYOUT;
@@ -60,7 +72,10 @@ export const PageLayoutInitializationQueryEffect = ({
pageLayoutId === DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID ||
pageLayoutId === DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID ||
pageLayoutId === DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID ||
pageLayoutId === DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID;
pageLayoutId === DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID ||
pageLayoutId === DEFAULT_WORKFLOW_PAGE_LAYOUT_ID ||
pageLayoutId === DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID ||
pageLayoutId === DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID;
const { data } = useQuery(FIND_ONE_PAGE_LAYOUT, {
variables: {
@@ -9,7 +9,7 @@ import {
type DropResult,
} from '@hello-pangea/dnd';
import { useId } from 'react';
import { type PageLayoutWidget } from '~/generated/graphql';
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
const StyledVerticalListContainer = styled.div`
display: flex;
@@ -69,7 +69,8 @@ export const PageLayoutVerticalListEditor = ({
<div {...provided.dragHandleProps}>
<WidgetRenderer
widget={widget}
widgetCardContext="recordPage"
pageLayoutType={PageLayoutType.RECORD_PAGE}
layoutMode="vertical-list"
/>
</div>
</StyledDraggableWrapper>
@@ -1,6 +1,6 @@
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
import styled from '@emotion/styled';
import { type PageLayoutWidget } from '~/generated/graphql';
import { PageLayoutType, type PageLayoutWidget } from '~/generated/graphql';
const StyledVerticalListContainer = styled.div`
display: flex;
@@ -19,7 +19,11 @@ export const PageLayoutVerticalListViewer = ({
<StyledVerticalListContainer>
{widgets.map((widget) => (
<div key={widget.id}>
<WidgetRenderer widget={widget} widgetCardContext="recordPage" />
<WidgetRenderer
widget={widget}
pageLayoutType={PageLayoutType.RECORD_PAGE}
layoutMode="vertical-list"
/>
</div>
))}
</StyledVerticalListContainer>