Fix a bunch of bugs on Record Page Layouts (#17342)

## Display task target and note target relations


https://github.com/user-attachments/assets/b291cc38-33b9-45b8-b685-e5890824176e

## Drop summary card in right drawer

### Before

<img width="1362" height="2160" alt="CleanShot 2026-01-22 at 14 27
09@2x"
src="https://github.com/user-attachments/assets/79f45d8f-976b-4999-8f62-5cb4b87b3ef6"
/>

### After

<img width="1362" height="2160" alt="CleanShot 2026-01-22 at 14 26
43@2x"
src="https://github.com/user-attachments/assets/fb67ddbb-0585-48bd-8ef6-c0139bba4062"
/>

## Add a tooltip to the see all action


https://github.com/user-attachments/assets/ab05cfab-8a6c-4caf-bfe9-698c8b7904e6

## Rename the Fields tab to Home

### Before

<img width="544" height="354" alt="CleanShot 2026-01-22 at 14 28 26@2x"
src="https://github.com/user-attachments/assets/62f7faa2-3a18-40af-8cdf-3fb9f7c7708c"
/>

### After

<img width="544" height="354" alt="CleanShot 2026-01-22 at 14 28 06@2x"
src="https://github.com/user-attachments/assets/76c21ec9-3723-4933-b34f-88b6403cbad5"
/>
This commit is contained in:
Baptiste Devessier
2026-01-22 17:17:10 +01:00
committed by GitHub
parent 267691a403
commit fb2c9e12bc
15 changed files with 120 additions and 99 deletions
@@ -1,7 +1,6 @@
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
import { PageLayoutTabHeader } from '@/page-layout/components/PageLayoutTabHeader';
import { PageLayoutTabList } from '@/page-layout/components/PageLayoutTabList';
import { PageLayoutTabListEffect } from '@/page-layout/components/PageLayoutTabListEffect';
import { PAGE_LAYOUT_LEFT_PANEL_CONTAINER_WIDTH } from '@/page-layout/constants/PageLayoutLeftPanelContainerWidth';
@@ -21,8 +20,8 @@ import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTab
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { t } from '@lingui/core/macro';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
@@ -129,8 +128,6 @@ export const PageLayoutRendererContent = () => {
/>
)}
<PageLayoutTabHeader />
<StyledScrollWrapper
componentInstanceId={getScrollWrapperInstanceIdFromPageLayoutId(
currentPageLayout.id,
@@ -1,28 +0,0 @@
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { PageLayoutType } from '~/generated/graphql';
export const PageLayoutTabHeader = () => {
const { currentPageLayout } = useCurrentPageLayout();
const targetRecordIdentifier = useTargetRecord();
const { isInRightDrawer } = useLayoutRenderingContext();
const isMobile = useIsMobile();
if (
currentPageLayout?.type !== PageLayoutType.RECORD_PAGE ||
!(isMobile || isInRightDrawer)
) {
return null;
}
return (
<SummaryCard
objectNameSingular={targetRecordIdentifier.targetObjectNameSingular}
objectRecordId={targetRecordIdentifier.id}
isInRightDrawer={isInRightDrawer}
/>
);
};
@@ -19,8 +19,8 @@ export const DEFAULT_COMPANY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'company-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID,
@@ -15,8 +15,8 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'note-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
@@ -19,8 +19,8 @@ export const DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'opportunity-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID,
@@ -15,8 +15,8 @@ export const DEFAULT_PERSON_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'person-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID,
@@ -15,8 +15,8 @@ export const DEFAULT_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'default-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_RECORD_PAGE_LAYOUT_ID,
@@ -15,8 +15,8 @@ export const DEFAULT_TASK_RECORD_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'task-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
@@ -21,10 +21,10 @@ export const DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'workflow-run-tab-fields',
title: 'Fields',
title: 'Home',
position: 100,
layoutMode: 'vertical-list',
icon: 'IconList',
icon: 'IconHome',
pageLayoutId: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -21,8 +21,8 @@ export const DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT: PageLayout = {
{
__typename: 'PageLayoutTab',
id: 'workflow-version-tab-fields',
title: 'Fields',
icon: 'IconList',
title: 'Home',
icon: 'IconHome',
position: 100,
layoutMode: 'vertical-list',
pageLayoutId: DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID,
@@ -3,30 +3,24 @@ import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguratio
import { useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useTemporaryFieldsConfiguration = (
objectNameSingular: string,
): FieldsConfiguration | null => {
): FieldsConfiguration => {
const { t } = useLingui();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const configuration = useMemo<FieldsConfiguration | null>(() => {
const configuration = useMemo<FieldsConfiguration>(() => {
if (!isDefined(objectMetadataItem)) {
return null;
throw new Error('Object metadata item is not defined');
}
const fieldsToDisplay = objectMetadataItem.fields.filter(
(field) =>
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION &&
field.type !== FieldMetadataType.RICH_TEXT_V2,
);
const fieldsToDisplay = objectMetadataItem.fields;
if (fieldsToDisplay.length === 0) {
return null;
throw new Error('No fields to display');
}
const generalFields: Array<{ fieldMetadataId: string; position: number }> =
@@ -72,7 +66,7 @@ export const useTemporaryFieldsConfiguration = (
}
if (sections.length === 0) {
return null;
throw new Error('No sections to display');
}
return {
@@ -9,16 +9,22 @@ import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { Link } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { IconArrowUpRight } from 'twenty-ui/display';
import {
AppTooltip,
IconArrowUpRight,
TooltipDelay,
TooltipPosition,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { RelationType } from '~/generated-metadata/graphql';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { Link } from 'react-router-dom';
const StyledLink = styled(Link)`
display: flex;
@@ -120,13 +126,30 @@ export const WidgetActionFieldSeeAll = () => {
filterQueryParams,
);
const tooltipId = `widget-see-all-${widget.id}`;
const relationLabelPlural =
relationObjectMetadataItem.labelPlural.toLowerCase();
const tooltipContent = t`See all ${relationLabelPlural} linked to this record`;
return (
<StyledLink to={filterLinkHref} data-testid="widget-see-all-link">
<StyledSeeAllButton
Icon={IconArrowUpRight}
accent="secondary"
isMobile={isMobile}
<>
<div id={tooltipId}>
<StyledLink to={filterLinkHref} data-testid="widget-see-all-link">
<StyledSeeAllButton
Icon={IconArrowUpRight}
accent="secondary"
isMobile={isMobile}
/>
</StyledLink>
</div>
<AppTooltip
anchorSelect={`#${tooltipId}`}
content={tooltipContent}
place={TooltipPosition.Top}
delay={TooltipDelay.mediumDelay}
offset={5}
noArrow
/>
</StyledLink>
</>
);
};
@@ -1,4 +1,6 @@
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
@@ -6,6 +8,7 @@ import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordF
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
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';
@@ -90,7 +93,9 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
targetRecord.targetObjectNameSingular,
);
if (sectionsWithFieldIndices.length === 0) {
const hasFieldsToDisplay = sectionsWithFieldIndices.length > 0;
if (!hasFieldsToDisplay) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
@@ -133,6 +138,11 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
<>
{section.fields.map(
({ field: fieldMetadataItem, globalIndex }) => {
const isActivityTarget = isActivityTargetField(
fieldMetadataItem.name,
targetRecord.targetObjectNameSingular,
);
return (
<FieldContext.Provider
key={targetRecord.id + fieldMetadataItem.id}
@@ -174,17 +184,37 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
})}`,
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<RecordInlineCell loading={recordLoading} />
</RecordFieldComponentInstanceContext.Provider>
{isActivityTarget ? (
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputInstanceId(
{
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
},
)}
activityObjectNameSingular={
targetRecord.targetObjectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={targetRecord.id}
showLabel={true}
maxWidth={200}
/>
) : (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<RecordInlineCell loading={recordLoading} />
</RecordFieldComponentInstanceContext.Provider>
)}
</FieldContext.Provider>
);
},
@@ -1,4 +1,3 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
type UseFieldsWidgetFieldMetadataItemsProps = {
@@ -7,13 +6,17 @@ type UseFieldsWidgetFieldMetadataItemsProps = {
export const useFieldsWidgetFieldMetadataItems = ({
objectNameSingular,
}: UseFieldsWidgetFieldMetadataItemsProps): FieldMetadataItem[] => {
const { inlineFieldMetadataItems } = useFieldListFieldMetadataItems({
objectNameSingular,
excludeFieldMetadataIds: [],
excludeCreatedAtAndUpdatedAt: true,
showRelationSections: false,
});
}: UseFieldsWidgetFieldMetadataItemsProps) => {
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
useFieldListFieldMetadataItems({
objectNameSingular,
excludeFieldMetadataIds: [],
excludeCreatedAtAndUpdatedAt: true,
showRelationSections: true,
});
return inlineFieldMetadataItems ?? [];
return {
inlineFieldMetadataItems,
legacyActivityTargetFieldMetadataItems,
};
};
@@ -10,21 +10,23 @@ export const useFieldsWidgetSectionsWithFields = (
) => {
const isMobile = useIsMobile();
const { isInRightDrawer } = useLayoutRenderingContext();
const fieldMetadataItems = useFieldsWidgetFieldMetadataItems({
objectNameSingular,
});
const { inlineFieldMetadataItems, legacyActivityTargetFieldMetadataItems } =
useFieldsWidgetFieldMetadataItems({
objectNameSingular,
});
const temporaryConfiguration =
useTemporaryFieldsConfiguration(objectNameSingular);
const context = buildWidgetVisibilityContext({ isMobile, isInRightDrawer });
const allFieldMetadataItems = [
...legacyActivityTargetFieldMetadataItems,
...inlineFieldMetadataItems,
];
const sectionsWithFields = filterAndOrderFieldsFromConfiguration({
configuration: temporaryConfiguration ?? {
__typename: 'FieldsConfiguration',
configurationType: 'FIELDS',
sections: [],
},
availableFieldMetadataItems: fieldMetadataItems,
configuration: temporaryConfiguration,
availableFieldMetadataItems: allFieldMetadataItems,
context,
});