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:
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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'}
|
||||||
|
|||||||
@ -233,6 +233,8 @@ export const useRecordShowPagePagination = (
|
|||||||
navigateToIndexView,
|
navigateToIndexView,
|
||||||
canNavigateToNextRecord,
|
canNavigateToNextRecord,
|
||||||
canNavigateToPreviousRecord,
|
canNavigateToPreviousRecord,
|
||||||
|
rankInView,
|
||||||
|
totalCount,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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, {
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export enum RecordTitleCellContainerType {
|
||||||
|
PageHeader = 'page-header',
|
||||||
|
ShowPage = 'show-page',
|
||||||
|
}
|
||||||
@ -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}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user