Various layout fixes so that RPL v1 look the same as production version (#17195)

> [!WARNING]
> Most of the quick fixes will be dropped once we release the feature
and deprecate the old show pages code.

This PR fixes padding and alignment issues so that the new Record Page
Layout feature looks like the old show pages.

## This PR

<img width="3456" height="2160" alt="CleanShot 2026-01-16 at 17 24
40@2x"
src="https://github.com/user-attachments/assets/f3640553-2316-498c-8dd0-14b09ed913f1"
/>

<img width="3456" height="2160" alt="CleanShot 2026-01-16 at 17 27
14@2x"
src="https://github.com/user-attachments/assets/c4ccb28f-5629-4e68-83ad-863f21066962"
/>


## The production

<img width="3456" height="2160" alt="CleanShot 2026-01-16 at 17 25
11@2x"
src="https://github.com/user-attachments/assets/a91f7216-0b57-404b-9f9d-f6f45d4858d6"
/>

<img width="3456" height="2234" alt="CleanShot 2026-01-16 at 17 27
19@2x"
src="https://github.com/user-attachments/assets/aaf1ee6b-b323-4844-9f1a-a59807cdbe40"
/>
This commit is contained in:
Baptiste Devessier
2026-01-16 18:06:34 +01:00
committed by GitHub
parent 8a27c4987d
commit 5bcbe43596
15 changed files with 189 additions and 59 deletions
@@ -1,13 +1,47 @@
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import isPropValid from '@emotion/is-prop-valid';
import styled from '@emotion/styled';
import { PageLayoutType } from '~/generated/graphql';
const StyledListItem = styled.div`
const StyledListItem = styled('div', {
shouldForwardProp: (prop) =>
isPropValid(prop) && prop !== 'noHorizontalPadding',
})<{ noHorizontalPadding?: boolean }>`
align-items: center;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing(1)};
display: flex;
height: ${({ theme }) => theme.spacing(10)};
padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme, noHorizontalPadding }) =>
noHorizontalPadding ? 0 : theme.spacing(3)};
padding-right: ${({ theme, noHorizontalPadding }) =>
noHorizontalPadding ? 0 : theme.spacing(2)};
`;
export { StyledListItem as RecordDetailRecordsListItemContainer };
type RecordDetailRecordsListItemContainerProps = {
children: React.ReactNode;
className?: string;
};
/**
* TODO: Remove noHorizontalPadding logic once the traditional record show page is removed.
*/
export const RecordDetailRecordsListItemContainer = ({
children,
className,
}: RecordDetailRecordsListItemContainerProps) => {
const layoutRenderingContext = useLayoutRenderingContext();
const isInRecordPageLayout =
layoutRenderingContext?.layoutType === PageLayoutType.RECORD_PAGE &&
!layoutRenderingContext?.isLegacyRecordShowPage;
return (
<StyledListItem
className={className}
noHorizontalPadding={isInRecordPageLayout}
>
{children}
</StyledListItem>
);
};
@@ -12,7 +12,9 @@ import { getCompaniesMock } from '~/testing/mock-data/companies';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { RecordDetailRelationSection } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { ComponentDecorator } from 'twenty-ui/testing';
import { PageLayoutType } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RightDrawerDecorator } from '~/testing/decorators/RightDrawerDecorator';
import { allMockPersonRecords } from '~/testing/mock-data/people';
@@ -34,29 +36,42 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
component: RecordDetailRelationSection,
decorators: [
(Story) => (
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'mock-instance-id' }}
<LayoutRenderingProvider
value={{
targetRecordIdentifier: {
id: companiesMock[0].id,
targetObjectNameSingular: 'company',
},
layoutType: PageLayoutType.RECORD_PAGE,
isInRightDrawer: false,
// TODO: Remove once the traditional record show page is removed.
isLegacyRecordShowPage: true,
}}
>
<FieldContext.Provider
value={{
recordId: companiesMock[0].id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: mockedCompanyObjectMetadataItem.fields.find(
({ name }) => name === 'people',
)!,
objectMetadataItem: mockedCompanyObjectMetadataItem,
}),
isRecordFieldReadOnly: false,
}}
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'mock-instance-id' }}
>
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: 'mock-instance-id' }}
<FieldContext.Provider
value={{
recordId: companiesMock[0].id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: mockedCompanyObjectMetadataItem.fields.find(
({ name }) => name === 'people',
)!,
objectMetadataItem: mockedCompanyObjectMetadataItem,
}),
isRecordFieldReadOnly: false,
}}
>
<Story />
</RecordFieldsScopeContextProvider>
</FieldContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: 'mock-instance-id' }}
>
<Story />
</RecordFieldsScopeContextProvider>
</FieldContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</LayoutRenderingProvider>
),
RightDrawerDecorator,
ComponentDecorator,
@@ -1,5 +1,7 @@
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import isPropValid from '@emotion/is-prop-valid';
import styled from '@emotion/styled';
import { PageLayoutType } from '~/generated/graphql';
interface PropertyBoxProps {
children: React.ReactNode;
@@ -8,8 +10,9 @@ interface PropertyBoxProps {
}
const StyledPropertyBoxContainer = styled('div', {
shouldForwardProp: isPropValid,
})`
shouldForwardProp: (prop) =>
isPropValid(prop) && prop !== 'noHorizontalPadding',
})<{ noHorizontalPadding?: boolean }>`
align-self: stretch;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
@@ -17,16 +20,33 @@ const StyledPropertyBoxContainer = styled('div', {
gap: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: ${({ theme }) => theme.spacing(3)};
padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme, noHorizontalPadding }) =>
noHorizontalPadding ? 0 : theme.spacing(3)};
padding-right: ${({ theme, noHorizontalPadding }) =>
noHorizontalPadding ? 0 : theme.spacing(2)};
`;
/**
* TODO: Remove noHorizontalPadding logic once the traditional record show page is removed.
*/
export const PropertyBox = ({
children,
className,
dataTestId,
}: PropertyBoxProps) => (
<StyledPropertyBoxContainer className={className} data-testid={dataTestId}>
{children}
</StyledPropertyBoxContainer>
);
}: PropertyBoxProps) => {
const layoutRenderingContext = useLayoutRenderingContext();
const isInRecordPageLayout =
layoutRenderingContext?.layoutType === PageLayoutType.RECORD_PAGE &&
!layoutRenderingContext?.isLegacyRecordShowPage;
return (
<StyledPropertyBoxContainer
className={className}
data-testid={dataTestId}
noHorizontalPadding={isInRecordPageLayout}
>
{children}
</StyledPropertyBoxContainer>
);
};
@@ -47,6 +47,8 @@ export const PageLayoutDispatcher = ({
},
layoutType: PageLayoutType.RECORD_PAGE,
isInRightDrawer,
// TODO: Remove once the traditional record show page is removed.
isLegacyRecordShowPage: true,
}}
>
<RecordShowContainer
@@ -1,10 +1,17 @@
import styled from '@emotion/styled';
import { useIsMobile } from 'twenty-ui/utilities';
import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant';
import { usePageLayoutShouldUseWhiteBackground } from '@/page-layout/hooks/usePageLayoutShouldUseWhiteBackground';
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 styled from '@emotion/styled';
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
const StyledVerticalListContainer = styled.div<{
shouldUseWhiteBackground: boolean;
variant: PageLayoutVerticalListViewerVariant;
}>`
background: ${({ theme, shouldUseWhiteBackground }) =>
shouldUseWhiteBackground
@@ -12,7 +19,8 @@ const StyledVerticalListContainer = styled.div<{
: theme.background.secondary};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
gap: ${({ theme, variant }) =>
variant === 'side-column' ? 0 : theme.spacing(2)};
`;
type PageLayoutVerticalListViewerProps = {
@@ -23,10 +31,20 @@ export const PageLayoutVerticalListViewer = ({
widgets,
}: PageLayoutVerticalListViewerProps) => {
const { shouldUseWhiteBackground } = usePageLayoutShouldUseWhiteBackground();
const { isInRightDrawer } = useLayoutRenderingContext();
const isMobile = useIsMobile();
const { isInPinnedTab } = useIsInPinnedTab();
const variant = getPageLayoutVerticalListViewerVariant({
isInPinnedTab,
isMobile,
isInRightDrawer,
});
return (
<StyledVerticalListContainer
shouldUseWhiteBackground={shouldUseWhiteBackground}
variant={variant}
>
{widgets.map((widget) => (
<div key={widget.id}>
@@ -0,0 +1,19 @@
import { type PageLayoutVerticalListViewerVariant } from '@/page-layout/types/PageLayoutVerticalListViewerVariant';
type GetPageLayoutVerticalListViewerVariantParams = {
isInPinnedTab: boolean;
isMobile: boolean;
isInRightDrawer: boolean;
};
export const getPageLayoutVerticalListViewerVariant = ({
isInPinnedTab,
isMobile,
isInRightDrawer,
}: GetPageLayoutVerticalListViewerVariantParams): PageLayoutVerticalListViewerVariant => {
if (isInPinnedTab || isMobile || isInRightDrawer) {
return 'side-column';
}
return 'default';
};
@@ -38,7 +38,7 @@ export const useTemporaryFieldsConfiguration = (
let otherPosition = 0;
fieldsToDisplay.forEach((field) => {
if (field.type === FieldMetadataType.LINKS) {
if (field.isCustom === true) {
otherFields.push({
fieldMetadataId: field.id,
position: otherPosition++,
@@ -0,0 +1 @@
export type PageLayoutVerticalListViewerVariant = 'default' | 'side-column';
@@ -53,6 +53,7 @@ export const DashboardWidgetPlaceholder = () => {
className="widget"
>
<WidgetCardHeader
variant="dashboard"
widgetId="widget-placeholder"
isInEditMode={isPageLayoutInEditMode}
isResizing={false}
@@ -84,7 +84,11 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const isRichTextWidget = widget.type === WidgetType.STANDALONE_RICH_TEXT;
const hideRichTextHeader = isRichTextWidget && !isPageLayoutInEditMode;
const showHeader = layoutMode !== 'canvas' && !hideRichTextHeader;
const showHeader =
layoutMode !== 'canvas' &&
!hideRichTextHeader &&
// TODO: use a more generic approach after record page layout v1 release
widget.type !== WidgetType.FIELDS;
const handleClick = () => {
handleEditWidget({
@@ -140,6 +144,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
{showHeader && (
<WidgetCardHeader
widgetId={widget.id}
variant={variant}
isInEditMode={isPageLayoutInEditMode}
isResizing={isResizing}
title={widget.title}
@@ -9,7 +9,6 @@ import { recordFieldListHoverPositionComponentState } from '@/object-record/reco
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
@@ -42,6 +41,16 @@ const StyledContainer = styled.div`
width: 100%;
`;
const StyledPropertyBox = styled.div`
align-self: stretch;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: ${({ theme }) => theme.spacing(3)};
`;
type FieldsWidgetProps = {
widget: PageLayoutWidget;
};
@@ -117,7 +126,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
key={section.id}
title={section.title}
>
<PropertyBox>
<StyledPropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
@@ -182,7 +191,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
)}
</>
)}
</PropertyBox>
</StyledPropertyBox>
</FieldsWidgetSectionContainer>
))}
@@ -4,24 +4,12 @@ import { useState } from 'react';
import { IconChevronDown } from 'twenty-ui/display';
import { AnimatedExpandableContainer, Section } from 'twenty-ui/layout';
const StyledFieldsWidgetSectionContainer = styled(Section)`
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: 0;
width: auto;
&:not(:first-of-type) {
padding-top: 0;
}
`;
const StyledHeader = styled.header`
align-items: center;
cursor: pointer;
display: flex;
height: 24px;
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledTitleLabel = styled.div`
@@ -53,7 +41,7 @@ export const FieldsWidgetSectionContainer = ({
setIsExpanded((previousIsExpanded) => !previousIsExpanded);
return (
<StyledFieldsWidgetSectionContainer>
<Section>
<StyledHeader onClick={handleToggleSection}>
<StyledTitleLabel>{title}</StyledTitleLabel>
<StyledChevronIcon
@@ -69,6 +57,6 @@ export const FieldsWidgetSectionContainer = ({
>
{children}
</AnimatedExpandableContainer>
</StyledFieldsWidgetSectionContainer>
</Section>
);
};
@@ -80,7 +80,9 @@ const StyledWidgetCard = styled.div<{
if (variant === 'side-column' && !isEditable) {
return css`
padding: ${theme.spacing(2)};
gap: ${theme.spacing(2)};
padding: ${theme.spacing(3)};
${isLastWidget !== true &&
css`
border-bottom: 1px solid ${theme.border.color.light};
@@ -2,18 +2,20 @@ import { WidgetActionRenderer } from '@/page-layout/widgets/components/WidgetAct
import { widgetCardHoveredComponentFamilyState } from '@/page-layout/widgets/states/widgetCardHoveredComponentFamilyState';
import { type WidgetAction } from '@/page-layout/widgets/types/WidgetAction';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useTheme } from '@emotion/react';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { type ReactNode } from 'react';
import { IconTrash, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { type WidgetCardVariant } from '@/page-layout/widgets/types/WidgetCardVariant';
import { WidgetGrip } from '@/page-layout/widgets/widget-card/components/WidgetGrip';
import { AnimatePresence, motion } from 'framer-motion';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
export type WidgetCardHeaderProps = {
variant: WidgetCardVariant;
widgetId: string;
isInEditMode: boolean;
isEmpty?: boolean;
@@ -32,14 +34,24 @@ const StyledWidgetCardHeader = styled.div`
flex-shrink: 0;
`;
const StyledTitleContainer = styled.div`
const StyledTitleContainer = styled.div<{ variant: WidgetCardVariant }>`
color: ${({ theme }) => theme.font.color.primary};
flex: 1;
font-size: ${({ theme }) => theme.font.size.md};
padding-inline: ${({ theme }) => theme.spacing(1)};
font-weight: ${({ theme }) => theme.font.weight.medium};
user-select: none;
overflow: hidden;
${({ theme, variant }) => {
switch (variant) {
case 'side-column':
return undefined;
default:
return css`
padding-inline: ${theme.spacing(1)};
`;
}
}}
`;
const StyledRightContainer = styled.div`
@@ -62,6 +74,7 @@ const StyledIconButtonContainer = styled(motion.div)`
export const WidgetCardHeader = ({
widgetId,
variant,
isEmpty = false,
isInEditMode = false,
isResizing = false,
@@ -88,7 +101,7 @@ export const WidgetCardHeader = ({
/>
)}
</AnimatePresence>
<StyledTitleContainer>
<StyledTitleContainer variant={variant}>
<OverflowingTextWithTooltip text={isEmpty ? t`Add Widget` : title} />
</StyledTitleContainer>
<StyledRightContainer>
@@ -11,6 +11,9 @@ export type LayoutRenderingContextType = {
layoutType: PageLayoutType;
isInRightDrawer: boolean;
/** TODO: Remove once the traditional record show page is removed. */
isLegacyRecordShowPage?: boolean;
};
export const [LayoutRenderingProvider, useLayoutRenderingContext] =