Fix mobile table styling outside record table (#10407) (#10663)

Fix to issue #10407 now including a fix to `ChipFieldDisplay` [throwing
an error outside a record
table](https://twenty-v7.sentry.io/issues/6350031213/?project=4507072563183616&referrer=github-pr-bot).

This works now, but refactoring is needed before merging.

Fixes #10407

---------

Co-authored-by: ad-elias <elias@autodiligence.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
eliasylonen
2025-04-01 17:11:31 +02:00
committed by GitHub
parent 8385e2d08b
commit 51ea241a1c
12 changed files with 142 additions and 101 deletions

View File

@ -22,6 +22,7 @@ export type RecordChipProps = {
maxWidth?: number; maxWidth?: number;
to?: string | undefined; to?: string | undefined;
size?: ChipSize; size?: ChipSize;
isLabelHidden?: boolean;
}; };
export const RecordChip = ({ export const RecordChip = ({
@ -33,6 +34,7 @@ export const RecordChip = ({
to, to,
size, size,
forceDisableClick = false, forceDisableClick = false,
isLabelHidden = false,
}: RecordChipProps) => { }: RecordChipProps) => {
const { recordChipData } = useRecordChipData({ const { recordChipData } = useRecordChipData({
objectNameSingular, objectNameSingular,
@ -74,6 +76,7 @@ export const RecordChip = ({
maxWidth={maxWidth} maxWidth={maxWidth}
placeholderColorSeed={record.id} placeholderColorSeed={record.id}
name={recordChipData.name} name={recordChipData.name}
isLabelHidden={isLabelHidden}
avatarType={recordChipData.avatarType} avatarType={recordChipData.avatarType}
avatarUrl={recordChipData.avatarUrl ?? ''} avatarUrl={recordChipData.avatarUrl ?? ''}
className={className} className={className}

View File

@ -14,6 +14,7 @@ export const FieldContextProvider = ({
fieldMetadataName, fieldMetadataName,
fieldPosition, fieldPosition,
isLabelIdentifier = false, isLabelIdentifier = false,
isLabelHidden,
objectNameSingular, objectNameSingular,
objectRecordId, objectRecordId,
customUseUpdateOneObjectHook, customUseUpdateOneObjectHook,
@ -24,6 +25,7 @@ export const FieldContextProvider = ({
fieldMetadataName: string; fieldMetadataName: string;
fieldPosition: number; fieldPosition: number;
isLabelIdentifier?: boolean; isLabelIdentifier?: boolean;
isLabelHidden?: boolean;
objectNameSingular: string; objectNameSingular: string;
objectRecordId: string; objectRecordId: string;
customUseUpdateOneObjectHook?: RecordUpdateHook; customUseUpdateOneObjectHook?: RecordUpdateHook;
@ -63,6 +65,7 @@ export const FieldContextProvider = ({
value={{ value={{
recordId: objectRecordId, recordId: objectRecordId,
isLabelIdentifier, isLabelIdentifier,
isLabelHidden,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({ fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem, field: fieldMetadataItem,
showLabel: true, showLabel: true,

View File

@ -34,6 +34,7 @@ export type GenericFieldContextType = {
isDisplayModeFixHeight?: boolean; isDisplayModeFixHeight?: boolean;
onOpenEditMode?: () => void; onOpenEditMode?: () => void;
onCloseEditMode?: () => void; onCloseEditMode?: () => void;
isLabelHidden?: boolean;
}; };
export const FieldContext = createContext<GenericFieldContextType>( export const FieldContext = createContext<GenericFieldContextType>(

View File

@ -4,8 +4,12 @@ import { ChipSize } from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
export const ChipFieldDisplay = () => { export const ChipFieldDisplay = () => {
const { recordValue, objectNameSingular, labelIdentifierLink } = const {
useChipFieldDisplay(); recordValue,
objectNameSingular,
labelIdentifierLink,
isLabelHidden,
} = useChipFieldDisplay();
if (!isDefined(recordValue)) { if (!isDefined(recordValue)) {
return null; return null;
@ -17,6 +21,7 @@ export const ChipFieldDisplay = () => {
record={recordValue} record={recordValue}
size={ChipSize.Small} size={ChipSize.Small}
to={labelIdentifierLink} to={labelIdentifierLink}
isLabelHidden={isLabelHidden}
/> />
); );
}; };

View File

@ -12,8 +12,13 @@ import { FieldContext } from '../../contexts/FieldContext';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
export const useChipFieldDisplay = () => { export const useChipFieldDisplay = () => {
const { recordId, fieldDefinition, isLabelIdentifier, labelIdentifierLink } = const {
useContext(FieldContext); recordId,
fieldDefinition,
isLabelIdentifier,
labelIdentifierLink,
isLabelHidden,
} = useContext(FieldContext);
const { chipGeneratorPerObjectPerField } = useContext( const { chipGeneratorPerObjectPerField } = useContext(
PreComputedChipGeneratorsContext, PreComputedChipGeneratorsContext,
@ -42,5 +47,6 @@ export const useChipFieldDisplay = () => {
recordValue, recordValue,
isLabelIdentifier, isLabelIdentifier,
labelIdentifierLink, labelIdentifierLink,
isLabelHidden,
}; };
}; };

View File

@ -1,5 +1,4 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui';
const StyledTbody = styled.tbody` const StyledTbody = styled.tbody`
&.first-columns-sticky { &.first-columns-sticky {
@ -22,18 +21,6 @@ const StyledTbody = styled.tbody`
z-index: 5; z-index: 5;
transition: 0.3s ease; transition: 0.3s ease;
@media (max-width: ${MOBILE_VIEWPORT}px) {
& [data-testid='editable-cell-display-mode'] {
[data-testid='tooltip'] {
display: none;
}
[data-testid='chip'] {
gap: 0;
}
}
}
&:not(.disable-shadow)::after { &:not(.disable-shadow)::after {
content: ''; content: '';
position: absolute; position: absolute;

View File

@ -0,0 +1,95 @@
import { ReactNode, useContext } from 'react';
import { useIsMobile } from 'twenty-ui';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { isRecordTableScrolledLeftComponentState } from '../../states/isRecordTableScrolledLeftComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
export const RecordTableCellFieldContext = ({
children,
}: {
children: ReactNode;
}) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId } = useRecordTableRowContextOrThrow();
const updateRecord = useContext(RecordUpdateContext);
const isMobile = useIsMobile();
const isRecordTableScrolledLeft = useRecoilComponentValueV2(
isRecordTableScrolledLeftComponentState,
);
const isLabelHidden =
isMobile &&
columnDefinition?.isLabelIdentifier &&
!isRecordTableScrolledLeft;
const computedHotkeyScope = (
columnDefinition: ColumnDefinition<FieldMetadata>,
) => {
if (isFieldRelation(columnDefinition)) {
if (
columnDefinition.metadata.relationType ===
RelationDefinitionType.MANY_TO_ONE
) {
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
}
if (
columnDefinition.metadata.relationType ===
RelationDefinitionType.ONE_TO_MANY
) {
return MultipleRecordPickerHotkeyScope.MultipleRecordPicker;
}
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
}
if (isFieldSelect(columnDefinition)) {
return SelectFieldHotkeyScope.SelectField;
}
return TableHotkeyScope.CellEditMode;
};
const customHotkeyScope = computedHotkeyScope(columnDefinition);
return (
<FieldContext.Provider
value={{
recordId,
fieldDefinition: columnDefinition,
useUpdateRecord: () => [updateRecord, {}],
hotkeyScope: customHotkeyScope,
labelIdentifierLink: indexIdentifierUrl(recordId),
isLabelIdentifier: isLabelIdentifierField({
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
name: columnDefinition.metadata.fieldName,
},
objectMetadataItem,
}),
displayedMaxRows: 1,
isLabelHidden,
}}
>
{children}
</FieldContext.Provider>
);
};

View File

@ -1,104 +1,32 @@
import { ReactNode, useContext } from 'react'; import { ReactNode, useContext } from 'react';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext'; import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { RecordTableCellFieldContext } from './RecordTableCellFieldContext';
export const RecordTableCellFieldContextWrapper = ({ export const RecordTableCellFieldContextWrapper = ({
children, children,
}: { }: {
children: ReactNode; children: ReactNode;
}) => { }) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext); const { columnDefinition } = useContext(RecordTableCellContext);
const { recordId } = useRecordTableRowContextOrThrow(); const { recordId } = useRecordTableRowContextOrThrow();
const updateRecord = useContext(RecordUpdateContext);
if (isUndefinedOrNull(columnDefinition)) { if (isUndefinedOrNull(columnDefinition)) {
return null; return null;
} }
// TODO: deprecate this and use useOpenFieldInput hooks to set the hotkey scope const instanceId = getRecordFieldInputId(
const computedHotkeyScope = ( recordId,
columnDefinition: ColumnDefinition<FieldMetadata>, columnDefinition.metadata.fieldName,
) => { 'record-table-cell',
if (isFieldRelation(columnDefinition)) { );
if (
columnDefinition.metadata.relationType ===
RelationDefinitionType.MANY_TO_ONE
) {
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
}
if (
columnDefinition.metadata.relationType ===
RelationDefinitionType.ONE_TO_MANY
) {
return MultipleRecordPickerHotkeyScope.MultipleRecordPicker;
}
return SingleRecordPickerHotkeyScope.SingleRecordPicker;
}
if (isFieldSelect(columnDefinition)) {
return SelectFieldHotkeyScope.SelectField;
}
return TableHotkeyScope.CellEditMode;
};
const customHotkeyScope = computedHotkeyScope(columnDefinition);
return ( return (
<FieldContext.Provider <RecordFieldComponentInstanceContext.Provider value={{ instanceId }}>
value={{ <RecordTableCellFieldContext>{children}</RecordTableCellFieldContext>
recordId, </RecordFieldComponentInstanceContext.Provider>
fieldDefinition: columnDefinition,
useUpdateRecord: () => [updateRecord, {}],
hotkeyScope: customHotkeyScope,
labelIdentifierLink: indexIdentifierUrl(recordId),
isLabelIdentifier: isLabelIdentifierField({
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
name: columnDefinition.metadata.fieldName,
},
objectMetadataItem,
}),
displayedMaxRows: 1,
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputId(
recordId,
columnDefinition.metadata.fieldName,
'record-table-cell',
),
}}
>
{children}
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
); );
}; };

View File

@ -1,6 +1,6 @@
import { ReactElement, useContext, useEffect, useRef } from 'react'; import { ReactElement, useContext, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconArrowUpRight } from 'twenty-ui'; import { IconArrowUpRight, useIsMobile } from 'twenty-ui';
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon'; import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty'; import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
@ -80,8 +80,12 @@ export const RecordTableCellSoftFocusMode = ({
? IconArrowUpRight // IconLayoutSidebarRightExpand - Disabling sidepanel access for now ? IconArrowUpRight // IconLayoutSidebarRightExpand - Disabling sidepanel access for now
: customButtonIcon; : customButtonIcon;
const isMobile = useIsMobile();
const showButton = const showButton =
isDefined(buttonIcon) && !editModeContentOnly && !isFieldReadOnly; isDefined(buttonIcon) &&
!editModeContentOnly &&
!isFieldReadOnly &&
!(isMobile && isFirstColumn);
const dontShowContent = isEmpty && isFieldReadOnly; const dontShowContent = isEmpty && isFieldReadOnly;

View File

@ -8,6 +8,7 @@ export type LinkAvatarChipProps = Omit<AvatarChipsCommonProps, 'clickable'> & {
to: string; to: string;
onClick?: LinkChipProps['onClick']; onClick?: LinkChipProps['onClick'];
variant?: AvatarChipVariant; variant?: AvatarChipVariant;
isLabelHidden?: boolean;
}; };
export const LinkAvatarChip = ({ export const LinkAvatarChip = ({
@ -24,11 +25,13 @@ export const LinkAvatarChip = ({
placeholderColorSeed, placeholderColorSeed,
size, size,
variant, variant,
isLabelHidden,
}: LinkAvatarChipProps) => ( }: LinkAvatarChipProps) => (
<LinkChip <LinkChip
to={to} to={to}
onClick={onClick} onClick={onClick}
label={name} label={name}
isLabelHidden={isLabelHidden}
variant={ variant={
//Regular but Highlighted -> missleading //Regular but Highlighted -> missleading
variant === AvatarChipVariant.Regular variant === AvatarChipVariant.Regular

View File

@ -26,6 +26,7 @@ export type ChipProps = {
disabled?: boolean; disabled?: boolean;
clickable?: boolean; clickable?: boolean;
label: string; label: string;
isLabelHidden?: boolean;
maxWidth?: number; maxWidth?: number;
variant?: ChipVariant; variant?: ChipVariant;
accent?: ChipAccent; accent?: ChipAccent;
@ -124,6 +125,7 @@ const StyledContainer = withTheme(styled.div<
export const Chip = ({ export const Chip = ({
size = ChipSize.Small, size = ChipSize.Small,
label, label,
isLabelHidden = false,
disabled = false, disabled = false,
clickable = true, clickable = true,
variant = ChipVariant.Regular, variant = ChipVariant.Regular,
@ -145,7 +147,9 @@ export const Chip = ({
maxWidth={maxWidth} maxWidth={maxWidth}
> >
{leftComponent?.()} {leftComponent?.()}
<OverflowingTextWithTooltip size={size} text={label} /> {!isLabelHidden && (
<OverflowingTextWithTooltip size={size} text={label} />
)}
{rightComponent?.()} {rightComponent?.()}
</StyledContainer> </StyledContainer>
); );

View File

@ -27,6 +27,7 @@ export const LinkChip = ({
to, to,
size = ChipSize.Small, size = ChipSize.Small,
label, label,
isLabelHidden = false,
variant = ChipVariant.Regular, variant = ChipVariant.Regular,
leftComponent = null, leftComponent = null,
rightComponent = null, rightComponent = null,
@ -40,6 +41,7 @@ export const LinkChip = ({
<Chip <Chip
size={size} size={size}
label={label} label={label}
isLabelHidden={isLabelHidden}
clickable={true} clickable={true}
variant={variant} variant={variant}
leftComponent={leftComponent} leftComponent={leftComponent}