Table small fixes (#14588)

This PR brings more UX fixes for table.

Fixes in https://github.com/twentyhq/core-team-issues/issues/1490 : 
- Fix any field filter chip formatting
- Empty placeholder should be centered on screenwidth and not scrollable
width on table
- First cell is automatically active on table but we can't move => We
now allow focus position to be null as it is a portal

Fixes https://github.com/twentyhq/twenty/issues/14573
This commit is contained in:
Lucas Bordeau
2025-09-18 11:29:33 +02:00
committed by GitHub
parent db8d8e8400
commit b4e386192d
14 changed files with 133 additions and 60 deletions
@@ -2,7 +2,10 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPe
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { type IconComponent } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
@@ -14,6 +17,18 @@ import {
type AnimatedPlaceholderType,
} from 'twenty-ui/layout';
const StyledEmptyPlaceholderOuterContainer = styled(
AnimatedPlaceholderEmptyContainer,
)`
align-items: flex-start;
`;
const StyledEmptyPlaceholderInnerContainer = styled(
AnimatedPlaceholderEmptyContainer,
)<{ width?: number }>`
width: ${({ width }) => (isDefined(width) ? `${width}px` : '100%')};
`;
type RecordTableEmptyStateDisplayButtonComponentProps = {
buttonComponent?: React.ReactNode;
};
@@ -50,29 +65,35 @@ export const RecordTableEmptyStateDisplay = (
hasAnySoftDeleteFilterOnViewComponentSelector,
);
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
const scrollWrapperWidth = scrollWrapperHTMLElement?.clientWidth;
return (
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type={props.animatedPlaceholderType} />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{props.title}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{props.subTitle}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{'buttonComponent' in props && props.buttonComponent}
{'buttonTitle' in props &&
!isReadOnly &&
!hasAnySoftDeleteFilterOnView && (
<Button
Icon={props.ButtonIcon}
title={props.buttonTitle}
variant="secondary"
onClick={props.onClick}
disabled={props.buttonIsDisabled}
/>
)}
</AnimatedPlaceholderEmptyContainer>
<StyledEmptyPlaceholderOuterContainer>
<StyledEmptyPlaceholderInnerContainer width={scrollWrapperWidth}>
<AnimatedPlaceholder type={props.animatedPlaceholderType} />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{props.title}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{props.subTitle}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{'buttonComponent' in props && props.buttonComponent}
{'buttonTitle' in props &&
!isReadOnly &&
!hasAnySoftDeleteFilterOnView && (
<Button
Icon={props.ButtonIcon}
title={props.buttonTitle}
variant="secondary"
onClick={props.onClick}
disabled={props.buttonIsDisabled}
/>
)}
</StyledEmptyPlaceholderInnerContainer>
</StyledEmptyPlaceholderOuterContainer>
);
};
@@ -4,6 +4,7 @@ import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useA
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useUnfocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useUnfocusRecordTableCell';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { recordTableHoverPositionComponentState } from '@/object-record/record-table/states/recordTableHoverPositionComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
@@ -21,6 +22,11 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
recordTableIdFromContext,
);
const setRecordTableFocusPosition = useSetRecoilComponentState(
recordTableFocusPositionComponentState,
recordTableIdFromContext,
);
const { unfocusRecordTableRow } = useFocusedRecordTableRow(
recordTableIdFromContext,
);
@@ -45,6 +51,7 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
deactivateRecordTableRow();
setRecordTableHoverPosition(null);
setRecordTableFocusPosition(null);
resetFocusStackToRecordIndex();
};
@@ -8,6 +8,7 @@ import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record
import { useFocusRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useFocusRecordTableCell';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { isDefined } from 'twenty-shared/utils';
export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
const { focusRecordTableCell } = useFocusRecordTableCell(recordTableId);
@@ -27,6 +28,10 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
() => {
const focusPosition = getSnapshotValue(snapshot, focusPositionState);
if (!isDefined(focusPosition)) {
return;
}
let newRowIndex = focusPosition.row - 1;
if (newRowIndex < 0) {
@@ -50,6 +55,10 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
);
const focusPosition = getSnapshotValue(snapshot, focusPositionState);
if (!isDefined(focusPosition)) {
return;
}
let newRowIndex = focusPosition.row + 1;
if (newRowIndex >= allRecordIds.length) {
@@ -76,8 +85,13 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
snapshot,
recordIndexAllRecordIdsSelector,
);
const focusPosition = getSnapshotValue(snapshot, focusPositionState);
if (!isDefined(focusPosition)) {
return;
}
const numberOfRecordFields = getSnapshotValue(
snapshot,
currentRecordFieldsCallbackState,
@@ -125,6 +139,10 @@ export const useRecordTableMoveFocusedCell = (recordTableId?: string) => {
() => {
const focusPosition = getSnapshotValue(snapshot, focusPositionState);
if (!isDefined(focusPosition)) {
return;
}
const numberOfRecordFields = getSnapshotValue(
snapshot,
currentRecordFieldsCallbackState,
@@ -1,11 +1,12 @@
import { useCurrentlyFocusedRecordTableCellFocusId } from '@/object-record/record-table/record-table-cell/hooks/useCurrentlyFocusedRecordTableCellFocusId';
import { useRecordTableCellFocusHotkeys } from '@/object-record/record-table/record-table-cell/hooks/useRecordTableCellFocusHotkeys';
export const RecordTableCellArrowKeysEffect = () => {
const recordTableCellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
export const RecordTableCellArrowKeysEffect = ({
cellFocusId,
}: {
cellFocusId: string;
}) => {
useRecordTableCellFocusHotkeys({
focusId: recordTableCellFocusId,
focusId: cellFocusId,
});
return null;
@@ -7,8 +7,10 @@ import { RecordTableCellEditMode } from '@/object-record/record-table/record-tab
import { RecordTableCellFieldInput } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput';
import { RecordTableCellHotkeysEffect } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect';
import { RecordTableCellPortalRootContainer } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalRootContainer';
import { useCurrentlyFocusedRecordTableCellFocusId } from '@/object-record/record-table/record-table-cell/hooks/useCurrentlyFocusedRecordTableCellFocusId';
import { recordTableCellEditModePositionComponentState } from '@/object-record/record-table/states/recordTableCellEditModePositionComponentState';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { isDefined } from 'twenty-shared/utils';
export const RecordTableCellEditModePortal = () => {
const focusedCellPosition = useRecoilComponentValue(
@@ -23,7 +25,9 @@ export const RecordTableCellEditModePortal = () => {
hasRecordGroupsComponentSelector,
);
if (!focusedCellPosition) {
const cellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
if (!isDefined(focusedCellPosition) || !isDefined(cellFocusId)) {
return null;
}
@@ -42,7 +46,7 @@ export const RecordTableCellEditModePortal = () => {
</RecordTableCellEditMode>
</RecordTableCellPortalRootContainer>
)}
<RecordTableCellHotkeysEffect />
<RecordTableCellHotkeysEffect cellFocusId={cellFocusId} />
</RecordTableCellPortalWrapper>
);
};
@@ -42,7 +42,7 @@ export const RecordTableCellFirstRowFirstColumn = ({
);
const isFocusPortalOnThisCell =
focusPosition.column === 0 && focusPosition.row === 0;
focusPosition?.column === 0 && focusPosition.row === 0;
const isHoveredPortalOnThisCell =
hoverPosition?.column === 0 && hoverPosition.row === 0;
@@ -9,6 +9,7 @@ import { recordTableHoverPositionComponentState } from '@/object-record/record-t
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { BORDER_COMMON } from 'twenty-ui/theme';
const StyledRecordTableCellFocusPortalContent = styled.div<{
@@ -47,11 +48,11 @@ export const RecordTableCellFocusedPortalContent = () => {
);
const arePositionsDifferent =
hoverPosition?.row !== focusPosition.row ||
hoverPosition?.column !== focusPosition.column;
hoverPosition?.row !== focusPosition?.row ||
hoverPosition?.column !== focusPosition?.column;
const handleContainerMouseMove = () => {
if (arePositionsDifferent) {
if (arePositionsDifferent && isDefined(focusPosition)) {
onMoveHoverToCurrentCell(focusPosition);
}
};
@@ -9,16 +9,18 @@ import { useToggleEditOnlyInput } from '@/object-record/record-field/ui/hooks/us
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useSelectAllRows } from '@/object-record/record-table/hooks/internal/useSelectAllRows';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
import { useCurrentlyFocusedRecordTableCellFocusId } from '@/object-record/record-table/record-table-cell/hooks/useCurrentlyFocusedRecordTableCellFocusId';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { useListenToSidePanelOpening } from '@/ui/layout/right-drawer/hooks/useListenToSidePanelOpening';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
export const RecordTableCellHotkeysEffect = () => {
export const RecordTableCellHotkeysEffect = ({
cellFocusId,
}: {
cellFocusId: string;
}) => {
const { openTableCell } = useOpenRecordTableCellFromCell();
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
const cellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
const { onCloseTableCell } = useRecordTableBodyContextOrThrow();
const isFieldInputOnly = useIsFieldInputOnly();
@@ -3,8 +3,10 @@ import { RecordTableCellArrowKeysEffect } from '@/object-record/record-table/rec
import { RecordTableCellEditModePortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellEditModePortal';
import { RecordTableCellFocusedPortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFocusedPortal';
import { RecordTableCellHoveredPortal } from '@/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal';
import { useCurrentlyFocusedRecordTableCellFocusId } from '@/object-record/record-table/record-table-cell/hooks/useCurrentlyFocusedRecordTableCellFocusId';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
export const RecordTableCellPortals = () => {
const { recordTableId } = useRecordTableContextOrThrow();
@@ -14,6 +16,8 @@ export const RecordTableCellPortals = () => {
recordTableId,
);
const recordTableCellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
return (
<>
<RecordTableCellHoveredPortal />
@@ -21,7 +25,11 @@ export const RecordTableCellPortals = () => {
{isRecordTableFocusActive && (
<>
<RecordTableCellEditModePortal />
<RecordTableCellArrowKeysEffect />
{isDefined(recordTableCellFocusId) && (
<RecordTableCellArrowKeysEffect
cellFocusId={recordTableCellFocusId}
/>
)}
</>
)}
</>
@@ -2,6 +2,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { getRecordTableCellFocusId } from '@/object-record/record-table/record-table-cell/utils/getRecordTableCellFocusId';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isDefined } from 'twenty-shared/utils';
export const useCurrentlyFocusedRecordTableCellFocusId = () => {
const { recordTableId } = useRecordTableContextOrThrow();
@@ -11,8 +12,10 @@ export const useCurrentlyFocusedRecordTableCellFocusId = () => {
recordTableId,
);
return getRecordTableCellFocusId({
recordTableId,
cellPosition: focusPosition,
});
return isDefined(focusPosition)
? getRecordTableCellFocusId({
recordTableId,
cellPosition: focusPosition,
})
: null;
};
@@ -7,6 +7,7 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type TableCellPosition } from '../../types/TableCellPosition';
import { useSetIsRecordTableCellFocusActive } from './useSetIsRecordTableCellFocusActive';
@@ -35,21 +36,26 @@ export const useFocusRecordTableCell = (recordTableId?: string) => {
.getLoadable(focusPositionState)
.getValue();
const currentCellFocusId = getRecordTableCellFocusId({
recordTableId: recordTableIdFromProps,
cellPosition: currentPosition,
});
if (isDefined(currentPosition)) {
const currentCellFocusId = getRecordTableCellFocusId({
recordTableId: recordTableIdFromProps,
cellPosition: currentPosition,
});
removeFocusItemFromFocusStackById({
focusId: currentCellFocusId,
});
removeFocusItemFromFocusStackById({
focusId: currentCellFocusId,
});
}
set(focusPositionState, newPosition);
setIsRecordTableCellFocusActive({
isRecordTableFocusActive: false,
cellPosition: currentPosition,
});
if (isDefined(currentPosition)) {
setIsRecordTableCellFocusActive({
isRecordTableFocusActive: false,
cellPosition: currentPosition,
});
}
setIsRecordTableCellFocusActive({
isRecordTableFocusActive: true,
cellPosition: newPosition,
@@ -6,6 +6,7 @@ import { recordTableFocusPositionComponentState } from '@/object-record/record-t
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { isDefined } from 'twenty-shared/utils';
import { useSetIsRecordTableCellFocusActive } from './useSetIsRecordTableCellFocusActive';
export const useUnfocusRecordTableCell = (recordTableId?: string) => {
@@ -32,6 +33,10 @@ export const useUnfocusRecordTableCell = (recordTableId?: string) => {
.getLoadable(focusPositionState)
.getValue();
if (!isDefined(currentPosition)) {
return;
}
const currentCellFocusId = getRecordTableCellFocusId({
recordTableId: recordTableIdFromProps,
cellPosition: currentPosition,
@@ -3,11 +3,8 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils
import { type TableCellPosition } from '../types/TableCellPosition';
export const recordTableFocusPositionComponentState =
createComponentState<TableCellPosition>({
createComponentState<TableCellPosition | null>({
key: 'recordTableFocusPositionComponentState',
defaultValue: {
row: 0,
column: 1,
},
defaultValue: null,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -23,8 +23,8 @@ export const AnyFieldSearchChip = () => {
return (
<SortOrFilterChip
testId={ADVANCED_FILTER_DROPDOWN_ID}
labelKey={t`Any field :`}
labelValue={anyFieldFilterValue}
labelKey={t`Any field`}
labelValue={`: ${anyFieldFilterValue}`}
Icon={IconFilter}
onRemove={handleRemoveClick}
type="filter"