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 { 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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -233,6 +233,8 @@ export const useRecordShowPagePagination = (
|
||||
navigateToIndexView,
|
||||
canNavigateToNextRecord,
|
||||
canNavigateToPreviousRecord,
|
||||
rankInView,
|
||||
totalCount,
|
||||
objectMetadataItem,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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,
|
||||
),
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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 = (
|
||||
recordId: string,
|
||||
fieldMetadataId: string,
|
||||
containerType: RecordTitleCellContainerType,
|
||||
) => {
|
||||
return `${recordId}-${fieldMetadataId}`;
|
||||
return `${recordId}-${fieldMetadataId}-${containerType}`;
|
||||
};
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user