834 Design Adjustments for the Record Page Breadcrumb (#11670)

# Design Adjustments for the Record Page Breadcrumb

Closes [834](https://github.com/twentyhq/core-team-issues/issues/834)
and [826](https://github.com/twentyhq/core-team-issues/issues/826)

## Description

- Added the breadcrumb to every object (not just the workflows)
- Fixed spacings
- Changed icon color from primary to tertiary for proper visual
hierarchy
- Displayed pagination information (current/total)
- Close button has been removed to simplify the UI
- Navigate to index page when the breadcrumb is clicked
- Fixed problems when two record title cells were displayed at the same
time (in the header and in the record page)

## Before

<img width="247" alt="Capture d’écran 2025-04-22 à 12 15 34"
src="https://github.com/user-attachments/assets/5ca2aca7-ffb0-49ea-8d3a-4bd621d78f8d"
/>

## After

<img width="233" alt="Capture d’écran 2025-04-22 à 12 15 06"
src="https://github.com/user-attachments/assets/cbcb5dfe-d616-47c9-8017-71dd4d388534"
/>
This commit is contained in:
Raphaël Bosi
2025-04-22 13:52:56 +02:00
committed by GitHub
parent 251a7b126b
commit 87083cb414
10 changed files with 76 additions and 38 deletions

View File

@ -3,7 +3,11 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions'; import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell'; import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
@ -17,20 +21,24 @@ const StyledEditableTitleContainer = styled.div`
`; `;
const StyledEditableTitlePrefix = styled.div` const StyledEditableTitlePrefix = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(0.75)};
`; `;
const StyledTitle = styled.div` const StyledTitle = styled.div`
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
padding-right: ${({ theme }) => theme.spacing(1)};
width: fit-content; width: fit-content;
`; `;
const StyledPaginationInformation = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const ObjectRecordShowPageBreadcrumb = ({ export const ObjectRecordShowPageBreadcrumb = ({
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
@ -59,13 +67,28 @@ export const ObjectRecordShowPageBreadcrumb = ({
recordId: objectRecordId, recordId: objectRecordId,
}); });
const { navigateToIndexView, rankInView, totalCount } =
useRecordShowPagePagination(objectNameSingular, objectRecordId);
const { headerIcon: HeaderIcon } = useRecordShowPage(
objectNameSingular,
objectRecordId,
);
const theme = useTheme();
if (loading) { if (loading) {
return null; return null;
} }
return ( return (
<StyledEditableTitleContainer> <StyledEditableTitleContainer>
<StyledEditableTitlePrefix> <StyledEditableTitlePrefix
onClick={() => {
navigateToIndexView();
}}
>
{HeaderIcon && <HeaderIcon size={theme.icon.size.md} />}
{capitalize(objectLabelPlural)} {capitalize(objectLabelPlural)}
<span>{' / '}</span> <span>{' / '}</span>
</StyledEditableTitlePrefix> </StyledEditableTitlePrefix>
@ -93,9 +116,15 @@ export const ObjectRecordShowPageBreadcrumb = ({
isReadOnly: isRecordReadOnly, isReadOnly: isRecordReadOnly,
}} }}
> >
<RecordTitleCell sizeVariant="xs" /> <RecordTitleCell
sizeVariant="xs"
containerType={RecordTitleCellContainerType.PageHeader}
/>
</FieldContext.Provider> </FieldContext.Provider>
</StyledTitle> </StyledTitle>
<StyledPaginationInformation>
{`(${rankInView + 1}/${totalCount})`}
</StyledPaginationInformation>
</StyledEditableTitleContainer> </StyledEditableTitleContainer>
); );
}; };

View File

@ -7,6 +7,7 @@ import { useRecordShowContainerData } from '@/object-record/record-show/hooks/us
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector'; import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell'; import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -94,7 +95,10 @@ export const SummaryCard = ({
isReadOnly: isRecordReadOnly, isReadOnly: isRecordReadOnly,
}} }}
> >
<RecordTitleCell sizeVariant="md" /> <RecordTitleCell
sizeVariant="md"
containerType={RecordTitleCellContainerType.ShowPage}
/>
</FieldContext.Provider> </FieldContext.Provider>
} }
avatarType={recordIdentifier?.avatarType ?? 'rounded'} avatarType={recordIdentifier?.avatarType ?? 'rounded'}

View File

@ -233,6 +233,8 @@ export const useRecordShowPagePagination = (
navigateToIndexView, navigateToIndexView,
canNavigateToNextRecord, canNavigateToNextRecord,
canNavigateToPreviousRecord, canNavigateToPreviousRecord,
rankInView,
totalCount,
objectMetadataItem, objectMetadataItem,
}; };
}; };

View File

@ -3,6 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell'; import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
@ -47,6 +48,7 @@ export const useCreateNewIndexRecord = ({
openRecordTitleCell({ openRecordTitleCell({
recordId, recordId,
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId, fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
containerType: RecordTitleCellContainerType.PageHeader,
}); });
} else { } else {
navigate(AppPath.RecordShowPage, { navigate(AppPath.RecordShowPage, {

View File

@ -18,23 +18,30 @@ import {
} from '@/object-record/record-title-cell/components/RecordTitleCellContext'; } from '@/object-record/record-title-cell/components/RecordTitleCellContext';
import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay'; import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay';
import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput'; import { RecordTitleCellFieldInput } from '@/object-record/record-title-cell/components/RecordTitleCellFieldInput';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId'; import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
type RecordTitleCellProps = { type RecordTitleCellProps = {
loading?: boolean; loading?: boolean;
sizeVariant?: 'xs' | 'md'; sizeVariant?: 'xs' | 'md';
containerType: RecordTitleCellContainerType;
}; };
export const RecordTitleCell = ({ export const RecordTitleCell = ({
loading, loading,
sizeVariant, sizeVariant,
containerType,
}: RecordTitleCellProps) => { }: RecordTitleCellProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext); const { fieldDefinition, recordId } = useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly(); const isFieldInputOnly = useIsFieldInputOnly();
const { closeInlineCell } = useInlineCell( const { closeInlineCell } = useInlineCell(
getRecordTitleCellId(recordId, fieldDefinition?.fieldMetadataId), getRecordTitleCellId(
recordId,
fieldDefinition?.fieldMetadataId,
containerType,
),
); );
const handleEnter: FieldInputEvent = (persistField) => { const handleEnter: FieldInputEvent = (persistField) => {
@ -83,6 +90,7 @@ export const RecordTitleCell = ({
instanceId: getRecordTitleCellId( instanceId: getRecordTitleCellId(
recordId, recordId,
fieldDefinition?.fieldMetadataId, fieldDefinition?.fieldMetadataId,
containerType,
), ),
}} }}
> >

View File

@ -1,5 +1,6 @@
import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey'; import { INLINE_CELL_HOTKEY_SCOPE_MEMOIZE_KEY } from '@/object-record/record-inline-cell/constants/InlineCellHotkeyScopeMemoizeKey';
import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState'; import { isInlineCellInEditModeScopedState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeScopedState';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId'; import { getRecordTitleCellId } from '@/object-record/record-title-cell/utils/getRecordTitleCellId';
import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope'; import { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
@ -21,13 +22,15 @@ export const useRecordTitleCell = () => {
({ ({
recordId, recordId,
fieldMetadataId, fieldMetadataId,
containerType,
}: { }: {
recordId: string; recordId: string;
fieldMetadataId: string; fieldMetadataId: string;
containerType: RecordTitleCellContainerType;
}) => { }) => {
set( set(
isInlineCellInEditModeScopedState( isInlineCellInEditModeScopedState(
getRecordTitleCellId(recordId, fieldMetadataId), getRecordTitleCellId(recordId, fieldMetadataId, containerType),
), ),
false, false,
); );
@ -44,15 +47,17 @@ export const useRecordTitleCell = () => {
({ ({
recordId, recordId,
fieldMetadataId, fieldMetadataId,
containerType,
customEditHotkeyScopeForField, customEditHotkeyScopeForField,
}: { }: {
recordId: string; recordId: string;
fieldMetadataId: string; fieldMetadataId: string;
containerType: RecordTitleCellContainerType;
customEditHotkeyScopeForField?: HotkeyScope; customEditHotkeyScopeForField?: HotkeyScope;
}) => { }) => {
set( set(
isInlineCellInEditModeScopedState( isInlineCellInEditModeScopedState(
getRecordTitleCellId(recordId, fieldMetadataId), getRecordTitleCellId(recordId, fieldMetadataId, containerType),
), ),
true, true,
); );

View File

@ -0,0 +1,4 @@
export enum RecordTitleCellContainerType {
PageHeader = 'page-header',
ShowPage = 'show-page',
}

View File

@ -1,6 +1,9 @@
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
export const getRecordTitleCellId = ( export const getRecordTitleCellId = (
recordId: string, recordId: string,
fieldMetadataId: string, fieldMetadataId: string,
containerType: RecordTitleCellContainerType,
) => { ) => {
return `${recordId}-${fieldMetadataId}`; return `${recordId}-${fieldMetadataId}-${containerType}`;
}; };

View File

@ -54,10 +54,10 @@ const StyledTitleContainer = styled.div`
display: flex; display: flex;
font-size: ${({ theme }) => theme.font.size.md}; font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(0.5)};
margin-right: ${({ theme }) => theme.spacing(1)}; margin-right: ${({ theme }) => theme.spacing(1)};
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
align-items: center;
`; `;
const StyledTopBarIconStyledTitleContainer = styled.div` const StyledTopBarIconStyledTitleContainer = styled.div`

View File

@ -1,8 +1,5 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields'; import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields';
import { ObjectRecordShowPageBreadcrumb } from '@/object-record/record-show/components/ObjectRecordShowPageBreadcrumb'; import { ObjectRecordShowPageBreadcrumb } from '@/object-record/record-show/components/ObjectRecordShowPageBreadcrumb';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination'; import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
import { PageHeader } from '@/ui/layout/page/components/PageHeader'; import { PageHeader } from '@/ui/layout/page/components/PageHeader';
@ -15,40 +12,24 @@ export const RecordShowPageHeader = ({
objectRecordId: string; objectRecordId: string;
children?: React.ReactNode; children?: React.ReactNode;
}) => { }) => {
const { viewName, navigateToIndexView, objectMetadataItem } = const { objectMetadataItem } = useRecordShowPagePagination(
useRecordShowPagePagination(objectNameSingular, objectRecordId); objectNameSingular,
objectRecordId,
const { headerIcon } = useRecordShowPage(objectNameSingular, objectRecordId);
const { layout } = useRecordShowContainerTabs(
false,
objectNameSingular as CoreObjectNameSingular,
false,
objectMetadataItem,
); );
const hasEditableName = layout.hideSummaryAndFields === true;
const { labelIdentifierFieldMetadataItem } = const { labelIdentifierFieldMetadataItem } =
getObjectMetadataIdentifierFields({ objectMetadataItem }); getObjectMetadataIdentifierFields({ objectMetadataItem });
return ( return (
<PageHeader <PageHeader
title={ title={
hasEditableName ? ( <ObjectRecordShowPageBreadcrumb
<ObjectRecordShowPageBreadcrumb objectNameSingular={objectNameSingular}
objectNameSingular={objectNameSingular} objectRecordId={objectRecordId}
objectRecordId={objectRecordId} objectLabelPlural={objectMetadataItem.labelPlural}
objectLabelPlural={objectMetadataItem.labelPlural} labelIdentifierFieldMetadataItem={labelIdentifierFieldMetadataItem}
labelIdentifierFieldMetadataItem={labelIdentifierFieldMetadataItem} />
/>
) : (
viewName
)
} }
hasClosePageButton
onClosePage={navigateToIndexView}
Icon={headerIcon}
> >
{children} {children}
</PageHeader> </PageHeader>