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 { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
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 { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
@ -17,20 +21,24 @@ const StyledEditableTitleContainer = styled.div`
`;
const StyledEditableTitlePrefix = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(0.75)};
`;
const StyledTitle = styled.div`
max-width: 100%;
overflow: hidden;
padding-right: ${({ theme }) => theme.spacing(1)};
width: fit-content;
`;
const StyledPaginationInformation = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const ObjectRecordShowPageBreadcrumb = ({
objectNameSingular,
objectRecordId,
@ -59,13 +67,28 @@ export const ObjectRecordShowPageBreadcrumb = ({
recordId: objectRecordId,
});
const { navigateToIndexView, rankInView, totalCount } =
useRecordShowPagePagination(objectNameSingular, objectRecordId);
const { headerIcon: HeaderIcon } = useRecordShowPage(
objectNameSingular,
objectRecordId,
);
const theme = useTheme();
if (loading) {
return null;
}
return (
<StyledEditableTitleContainer>
<StyledEditableTitlePrefix>
<StyledEditableTitlePrefix
onClick={() => {
navigateToIndexView();
}}
>
{HeaderIcon && <HeaderIcon size={theme.icon.size.md} />}
{capitalize(objectLabelPlural)}
<span>{' / '}</span>
</StyledEditableTitlePrefix>
@ -93,9 +116,15 @@ export const ObjectRecordShowPageBreadcrumb = ({
isReadOnly: isRecordReadOnly,
}}
>
<RecordTitleCell sizeVariant="xs" />
<RecordTitleCell
sizeVariant="xs"
containerType={RecordTitleCellContainerType.PageHeader}
/>
</FieldContext.Provider>
</StyledTitle>
<StyledPaginationInformation>
{`(${rankInView + 1}/${totalCount})`}
</StyledPaginationInformation>
</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 { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValue } from 'recoil';
@ -94,7 +95,10 @@ export const SummaryCard = ({
isReadOnly: isRecordReadOnly,
}}
>
<RecordTitleCell sizeVariant="md" />
<RecordTitleCell
sizeVariant="md"
containerType={RecordTitleCellContainerType.ShowPage}
/>
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}

View File

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

View File

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

View File

@ -18,23 +18,30 @@ import {
} from '@/object-record/record-title-cell/components/RecordTitleCellContext';
import { RecordTitleCellFieldDisplay } from '@/object-record/record-title-cell/components/RecordTitleCellFieldDisplay';
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';
type RecordTitleCellProps = {
loading?: boolean;
sizeVariant?: 'xs' | 'md';
containerType: RecordTitleCellContainerType;
};
export const RecordTitleCell = ({
loading,
sizeVariant,
containerType,
}: RecordTitleCellProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly();
const { closeInlineCell } = useInlineCell(
getRecordTitleCellId(recordId, fieldDefinition?.fieldMetadataId),
getRecordTitleCellId(
recordId,
fieldDefinition?.fieldMetadataId,
containerType,
),
);
const handleEnter: FieldInputEvent = (persistField) => {
@ -83,6 +90,7 @@ export const RecordTitleCell = ({
instanceId: getRecordTitleCellId(
recordId,
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 { 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 { TitleInputHotkeyScope } from '@/ui/input/types/TitleInputHotkeyScope';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
@ -21,13 +22,15 @@ export const useRecordTitleCell = () => {
({
recordId,
fieldMetadataId,
containerType,
}: {
recordId: string;
fieldMetadataId: string;
containerType: RecordTitleCellContainerType;
}) => {
set(
isInlineCellInEditModeScopedState(
getRecordTitleCellId(recordId, fieldMetadataId),
getRecordTitleCellId(recordId, fieldMetadataId, containerType),
),
false,
);
@ -44,15 +47,17 @@ export const useRecordTitleCell = () => {
({
recordId,
fieldMetadataId,
containerType,
customEditHotkeyScopeForField,
}: {
recordId: string;
fieldMetadataId: string;
containerType: RecordTitleCellContainerType;
customEditHotkeyScopeForField?: HotkeyScope;
}) => {
set(
isInlineCellInEditModeScopedState(
getRecordTitleCellId(recordId, fieldMetadataId),
getRecordTitleCellId(recordId, fieldMetadataId, containerType),
),
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 = (
recordId: string,
fieldMetadataId: string,
containerType: RecordTitleCellContainerType,
) => {
return `${recordId}-${fieldMetadataId}`;
return `${recordId}-${fieldMetadataId}-${containerType}`;
};

View File

@ -54,10 +54,10 @@ const StyledTitleContainer = styled.div`
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(0.5)};
margin-right: ${({ theme }) => theme.spacing(1)};
width: 100%;
overflow: hidden;
align-items: center;
`;
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 { 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 { PageHeader } from '@/ui/layout/page/components/PageHeader';
@ -15,40 +12,24 @@ export const RecordShowPageHeader = ({
objectRecordId: string;
children?: React.ReactNode;
}) => {
const { viewName, navigateToIndexView, objectMetadataItem } =
useRecordShowPagePagination(objectNameSingular, objectRecordId);
const { headerIcon } = useRecordShowPage(objectNameSingular, objectRecordId);
const { layout } = useRecordShowContainerTabs(
false,
objectNameSingular as CoreObjectNameSingular,
false,
objectMetadataItem,
const { objectMetadataItem } = useRecordShowPagePagination(
objectNameSingular,
objectRecordId,
);
const hasEditableName = layout.hideSummaryAndFields === true;
const { labelIdentifierFieldMetadataItem } =
getObjectMetadataIdentifierFields({ objectMetadataItem });
return (
<PageHeader
title={
hasEditableName ? (
<ObjectRecordShowPageBreadcrumb
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
objectLabelPlural={objectMetadataItem.labelPlural}
labelIdentifierFieldMetadataItem={labelIdentifierFieldMetadataItem}
/>
) : (
viewName
)
<ObjectRecordShowPageBreadcrumb
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
objectLabelPlural={objectMetadataItem.labelPlural}
labelIdentifierFieldMetadataItem={labelIdentifierFieldMetadataItem}
/>
}
hasClosePageButton
onClosePage={navigateToIndexView}
Icon={headerIcon}
>
{children}
</PageHeader>