diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx
index bc707ff323..ea6d02ccf4 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx
@@ -24,13 +24,13 @@ import { getActivityAttachmentIdsToDelete } from '@/activities/utils/getActivity
import { getActivityAttachmentPathsToRestore } from '@/activities/utils/getActivityAttachmentPathsToRestore';
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
+import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState';
-import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
@@ -363,10 +363,10 @@ export const ActivityRichTextEditor = ({
dependencies: [handleAllKeys],
});
- const { labelIdentifierFieldMetadataItem } = useRecordShowContainerData({
- objectNameSingular: activityObjectNameSingular,
- objectRecordId: activityId,
- });
+ const { labelIdentifierFieldMetadataItem } =
+ useLabelIdentifierFieldMetadataItem({
+ objectNameSingular: activityObjectNameSingular,
+ });
const recordTitleCellId = getRecordFieldInputInstanceId({
recordId: activityId,
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchorPortalContext.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchorPortalContext.tsx
new file mode 100644
index 0000000000..7f105c7049
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchorPortalContext.tsx
@@ -0,0 +1,56 @@
+import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
+import { FieldInput } from '@/object-record/record-field/ui/components/FieldInput';
+import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
+import { useGetButtonIcon } from '@/object-record/record-field/ui/hooks/useGetButtonIcon';
+import { useIsFieldInputOnly } from '@/object-record/record-field/ui/hooks/useIsFieldInputOnly';
+import {
+ RecordInlineCellContext,
+ type RecordInlineCellContextProps,
+} from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
+import { useContext, type ReactNode } from 'react';
+import { useIcons } from 'twenty-ui/display';
+
+type RecordFieldListCellAnchorPortalContextProps = {
+ children: ReactNode;
+};
+
+export const RecordFieldListCellAnchorPortalContext = ({
+ children,
+}: RecordFieldListCellAnchorPortalContextProps) => {
+ const {
+ isRecordFieldReadOnly,
+ fieldDefinition,
+ isDisplayModeFixHeight,
+ onOpenEditMode,
+ onCloseEditMode,
+ isCentered,
+ } = useContext(FieldContext);
+ const buttonIcon = useGetButtonIcon();
+ const { getIcon } = useIcons();
+ const isFieldInputOnly = useIsFieldInputOnly();
+
+ const RecordInlineCellContextValue: RecordInlineCellContextProps = {
+ readonly: isRecordFieldReadOnly,
+ buttonIcon: buttonIcon,
+ IconLabel: fieldDefinition.iconName
+ ? getIcon(fieldDefinition.iconName)
+ : undefined,
+ label: fieldDefinition.label,
+ labelWidth: fieldDefinition.labelWidth,
+ showLabel: fieldDefinition.showLabel,
+ isCentered,
+ editModeContent: ,
+ displayModeContent: ,
+ isDisplayModeFixHeight: isDisplayModeFixHeight,
+ editModeContentOnly: isFieldInputOnly,
+ loading: false,
+ onOpenEditMode,
+ onCloseEditMode,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchoredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchoredPortal.tsx
new file mode 100644
index 0000000000..1de8a86861
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchoredPortal.tsx
@@ -0,0 +1,118 @@
+import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
+import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
+import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
+import { RecordFieldListCellAnchorPortalContext } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchorPortalContext';
+import {
+ FieldContext,
+ type RecordUpdateHook,
+ type RecordUpdateHookParams,
+} from '@/object-record/record-field/ui/contexts/FieldContext';
+import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
+import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
+import { RecordInlineCellHoveredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellHoveredPortal';
+import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { createPortal } from 'react-dom';
+import { isDefined } from 'twenty-shared/utils';
+
+type RecordFieldListCellAnchoredPortalProps = {
+ position: number;
+ fieldMetadataItem: Pick<
+ FieldMetadataItem,
+ 'id' | 'name' | 'type' | 'createdAt' | 'updatedAt' | 'label'
+ >;
+ children: React.ReactNode;
+};
+
+export const RecordFieldListCellAnchoredPortal = ({
+ position,
+ fieldMetadataItem,
+ children,
+}: RecordFieldListCellAnchoredPortalProps) => {
+ const anchorElement = document.body.querySelector(
+ `#record-field-list-cell-${position}`,
+ );
+
+ const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
+
+ const targetedRecordsRuleFromContextStore = useRecoilComponentValue(
+ contextStoreTargetedRecordsRuleComponentState,
+ );
+
+ let recordId;
+
+ if (targetedRecordsRuleFromContextStore.mode === 'selection') {
+ recordId = targetedRecordsRuleFromContextStore.selectedRecordIds[0];
+ }
+
+ const INPUT_ID_PREFIX = 'fields-card';
+
+ const isRecordFieldReadOnly = useIsRecordFieldReadOnly({
+ fieldMetadataId: fieldMetadataItem.id,
+ objectMetadataId: objectMetadataItem.id,
+ recordId: recordId ?? '',
+ });
+
+ const { updateOneRecord } = useUpdateOneRecord({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
+ const updateEntity = ({ variables }: RecordUpdateHookParams) => {
+ updateOneRecord?.({
+ idToUpdate: variables.where.id as string,
+ updateOneRecordInput: variables.updateOneRecordInput,
+ });
+ };
+
+ return [updateEntity, { loading: false }];
+ };
+
+ if (!isDefined(anchorElement) || !isDefined(recordId)) {
+ return null;
+ }
+
+ return (
+
+ <>
+ {createPortal(
+
+
+
+ {children}
+
+
+ ,
+ anchorElement,
+ )}
+ >
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal.tsx
new file mode 100644
index 0000000000..06884a0c87
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal.tsx
@@ -0,0 +1,39 @@
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+
+import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { RecordFieldListCellAnchoredPortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellAnchoredPortal';
+import { RecordFieldListCellHoveredPortalContent } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent';
+import { useFieldListFieldMetadataFromPosition } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition';
+import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
+import { isDefined } from 'twenty-shared/utils';
+
+type RecordFieldListCellHoveredPortalProps = {
+ objectMetadataItem: ObjectMetadataItem;
+};
+
+export const RecordFieldListCellHoveredPortal = ({
+ objectMetadataItem,
+}: RecordFieldListCellHoveredPortalProps) => {
+ const hoverPosition = useRecoilComponentValue(
+ recordFieldListHoverPositionComponentState,
+ );
+
+ const { hoveredFieldMetadataItem } = useFieldListFieldMetadataFromPosition({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ if (!isDefined(hoverPosition) || !isDefined(hoveredFieldMetadataItem)) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent.tsx
new file mode 100644
index 0000000000..021c4c0c38
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent.tsx
@@ -0,0 +1,65 @@
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+
+import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { useFieldListFieldMetadataFromPosition } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition';
+import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
+import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
+import { FieldInput } from '@/object-record/record-field/ui/components/FieldInput';
+import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
+import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
+import { RecordInlineCellDisplayMode } from '@/object-record/record-inline-cell/components/RecordInlineCellDisplayMode';
+import { RecordInlineCellHoveredPortalContent } from '@/object-record/record-inline-cell/components/RecordInlineCellHoveredPortalContent';
+import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
+import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
+import { useContext } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+type RecordFieldListCellHoveredPortalContentProps = {
+ objectMetadataItem: ObjectMetadataItem;
+};
+
+export const RecordFieldListCellHoveredPortalContent = ({
+ objectMetadataItem,
+}: RecordFieldListCellHoveredPortalContentProps) => {
+ const hoverPosition = useRecoilComponentValue(
+ recordFieldListHoverPositionComponentState,
+ );
+
+ const { editModeContentOnly, isCentered } = useRecordInlineCellContext();
+
+ const { hoveredFieldMetadataItem } = useFieldListFieldMetadataFromPosition({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const { isRecordFieldReadOnly } = useContext(FieldContext);
+ const { openInlineCell } = useInlineCell();
+
+ const shouldContainerBeClickable =
+ !isRecordFieldReadOnly && !editModeContentOnly;
+
+ const setRecordFieldListHoverPosition = useSetRecoilComponentState(
+ recordFieldListHoverPositionComponentState,
+ 'fields-card',
+ );
+
+ const handleMouseLeave = () => {
+ setRecordFieldListHoverPosition(null);
+ };
+
+ if (!isDefined(hoverPosition) || !isDefined(hoveredFieldMetadataItem)) {
+ return null;
+ }
+
+ return (
+
+
+ {editModeContentOnly ? : }
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldListCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldListCellFieldInput.tsx
new file mode 100644
index 0000000000..4023e4b0ac
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldListCellFieldInput.tsx
@@ -0,0 +1,110 @@
+import { FieldInput } from '@/object-record/record-field/ui/components/FieldInput';
+
+import { usePersistFieldFromFieldInputContext } from '@/object-record/record-field/ui/hooks/usePersistFieldFromFieldInputContext';
+import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
+
+import {
+ FieldInputEventContext,
+ type FieldInputClickOutsideEvent,
+ type FieldInputEvent,
+} from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
+import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
+import { currentFocusIdSelector } from '@/ui/utilities/focus/states/currentFocusIdSelector';
+import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
+import { useRecoilCallback } from 'recoil';
+
+export const RecordTableCellFieldInput = () => {
+ const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
+
+ const instanceId = useAvailableComponentInstanceId(
+ RecordFieldComponentInstanceContext,
+ );
+
+ const { persistFieldFromFieldInputContext } =
+ usePersistFieldFromFieldInputContext();
+
+ const handleEnter: FieldInputEvent = ({ newValue, skipPersist }) => {
+ if (skipPersist !== true) {
+ persistFieldFromFieldInputContext(newValue);
+ }
+
+ onCloseTableCell();
+ onMoveFocus('down');
+ };
+
+ const handleSubmit: FieldInputEvent = ({ newValue, skipPersist }) => {
+ if (skipPersist !== true) {
+ persistFieldFromFieldInputContext(newValue);
+ }
+
+ onCloseTableCell();
+ };
+
+ const handleCancel = () => {
+ onCloseTableCell();
+ };
+
+ const handleClickOutside: FieldInputClickOutsideEvent = useRecoilCallback(
+ ({ snapshot }) =>
+ ({ newValue, event, skipPersist }) => {
+ const currentFocusId = snapshot
+ .getLoadable(currentFocusIdSelector)
+ .getValue();
+
+ if (currentFocusId !== instanceId) {
+ return;
+ }
+ event?.preventDefault();
+ event?.stopImmediatePropagation();
+
+ if (skipPersist !== true) {
+ persistFieldFromFieldInputContext(newValue);
+ }
+
+ onCloseTableCell();
+ },
+ [onCloseTableCell, instanceId, persistFieldFromFieldInputContext],
+ );
+
+ const handleEscape: FieldInputEvent = ({ newValue, skipPersist }) => {
+ if (skipPersist !== true) {
+ persistFieldFromFieldInputContext(newValue);
+ }
+
+ onCloseTableCell();
+ };
+
+ const handleTab: FieldInputEvent = ({ newValue, skipPersist }) => {
+ if (skipPersist !== true) {
+ persistFieldFromFieldInputContext(newValue);
+ }
+
+ onCloseTableCell();
+ onMoveFocus('right');
+ };
+
+ const handleShiftTab: FieldInputEvent = ({ newValue, skipPersist }) => {
+ if (skipPersist !== true) {
+ persistFieldFromFieldInputContext(newValue);
+ }
+
+ onCloseTableCell();
+ onMoveFocus('left');
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition.ts b/packages/twenty-front/src/modules/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition.ts
new file mode 100644
index 0000000000..7b10e934ea
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition.ts
@@ -0,0 +1,48 @@
+import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
+import { recordFieldListCellEditModePositionComponentState } from '@/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState';
+import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { isDefined } from 'twenty-shared/utils';
+
+type UseFieldListFieldMetadataFromPositionProps = {
+ objectNameSingular: string;
+};
+
+export const useFieldListFieldMetadataFromPosition = ({
+ objectNameSingular,
+}: UseFieldListFieldMetadataFromPositionProps) => {
+ const hoverPosition = useRecoilComponentValue(
+ recordFieldListHoverPositionComponentState,
+ );
+
+ const editModePosition = useRecoilComponentValue(
+ recordFieldListCellEditModePositionComponentState,
+ );
+
+ const {
+ inlineFieldMetadataItems,
+ inlineRelationFieldMetadataItems,
+ boxedRelationFieldMetadataItems,
+ } = useFieldListFieldMetadataItems({
+ objectNameSingular,
+ });
+
+ const fieldMetadataItems = [
+ ...inlineRelationFieldMetadataItems,
+ ...inlineFieldMetadataItems,
+ ...boxedRelationFieldMetadataItems,
+ ];
+
+ const hoveredFieldMetadataItem = isDefined(hoverPosition)
+ ? fieldMetadataItems.at(hoverPosition)
+ : undefined;
+
+ const editedFieldMetadataItem = isDefined(editModePosition)
+ ? fieldMetadataItems.at(editModePosition)
+ : undefined;
+
+ return {
+ hoveredFieldMetadataItem,
+ editedFieldMetadataItem,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/hooks/useFieldListFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-record/record-field-list/hooks/useFieldListFieldMetadataItems.ts
new file mode 100644
index 0000000000..a5a8cc9f7f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/hooks/useFieldListFieldMetadataItems.ts
@@ -0,0 +1,91 @@
+import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
+import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
+import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
+import groupBy from 'lodash.groupby';
+import { FieldMetadataType } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+type UseFieldListFieldMetadataItemsProps = {
+ objectNameSingular: string;
+};
+
+export const useFieldListFieldMetadataItems = ({
+ objectNameSingular,
+}: UseFieldListFieldMetadataItemsProps) => {
+ const { labelIdentifierFieldMetadataItem } =
+ useLabelIdentifierFieldMetadataItem({
+ objectNameSingular,
+ });
+
+ const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
+
+ const { objectMetadataItem } = useObjectMetadataItem({
+ objectNameSingular,
+ });
+
+ const { objectMetadataItems } = useObjectMetadataItems();
+
+ const availableFieldMetadataItems = objectMetadataItem.readableFields
+ .filter(
+ (fieldMetadataItem) =>
+ isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
+ fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
+ )
+ .sort((fieldMetadataItemA, fieldMetadataItemB) =>
+ fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
+ );
+
+ const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
+ availableFieldMetadataItems
+ .filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.name !== 'createdAt' &&
+ fieldMetadataItem.name !== 'deletedAt',
+ )
+ .filter(
+ (fieldMetadataItem) =>
+ fieldMetadataItem.type !== FieldMetadataType.RICH_TEXT_V2,
+ ),
+ (fieldMetadataItem) =>
+ fieldMetadataItem.type === FieldMetadataType.RELATION
+ ? 'relationFieldMetadataItems'
+ : 'inlineFieldMetadataItems',
+ );
+
+ const inlineRelationFieldMetadataItems = (
+ relationFieldMetadataItems ?? []
+ ).filter(
+ (fieldMetadataItem) =>
+ (objectNameSingular === CoreObjectNameSingular.Note &&
+ fieldMetadataItem.name === 'noteTargets') ||
+ (objectNameSingular === CoreObjectNameSingular.Task &&
+ fieldMetadataItem.name === 'taskTargets'),
+ );
+
+ const boxedRelationFieldMetadataItems = (
+ relationFieldMetadataItems ?? []
+ ).filter(
+ (fieldMetadataItem) =>
+ !(
+ (objectNameSingular === CoreObjectNameSingular.Note &&
+ fieldMetadataItem.name === 'noteTargets') ||
+ (objectNameSingular === CoreObjectNameSingular.Task &&
+ fieldMetadataItem.name === 'taskTargets')
+ ) &&
+ isDefined(fieldMetadataItem.relation?.targetObjectMetadata.id) &&
+ getObjectPermissionsForObject(
+ objectPermissionsByObjectMetadataId,
+ fieldMetadataItem.relation?.targetObjectMetadata.id,
+ ).canReadObjectRecords,
+ );
+
+ return {
+ inlineFieldMetadataItems,
+ inlineRelationFieldMetadataItems,
+ boxedRelationFieldMetadataItems,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext.ts
new file mode 100644
index 0000000000..f2eceb8863
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext.ts
@@ -0,0 +1,4 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const RecordFieldListComponentInstanceContext =
+ createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState.ts
new file mode 100644
index 0000000000..d784a2eaa7
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState.ts
@@ -0,0 +1,9 @@
+import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
+import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
+
+export const recordFieldListCellEditModePositionComponentState =
+ createComponentState({
+ key: 'recordFieldListCellEditModePositionComponentState',
+ defaultValue: null,
+ componentInstanceContext: RecordFieldListComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/states/recordFieldListHoverPositionComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field-list/states/recordFieldListHoverPositionComponentState.ts
new file mode 100644
index 0000000000..f949c84804
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/states/recordFieldListHoverPositionComponentState.ts
@@ -0,0 +1,10 @@
+import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
+import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
+
+export const recordFieldListHoverPositionComponentState = createComponentState<
+ number | null
+>({
+ key: 'recordFieldListHoverPositionComponentState',
+ defaultValue: null,
+ componentInstanceContext: RecordFieldListComponentInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/contexts/FieldContext.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/contexts/FieldContext.ts
index 6fe7e7fdf1..c760cfb9c8 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/contexts/FieldContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/contexts/FieldContext.ts
@@ -38,8 +38,11 @@ export type GenericFieldContextType = {
onRecordChipClick?: (event: MouseEvent) => void;
onOpenEditMode?: () => void;
onCloseEditMode?: () => void;
+ onMouseEnter?: () => void;
+ onMouseLeave?: () => void;
triggerEvent?: TriggerEventType;
isForbidden?: boolean;
+ anchorId?: string;
};
export const FieldContext = createContext(
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
index 7144f1c2a2..394c3a4261 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
@@ -99,7 +99,8 @@ export const RecordInlineCellContainer = () => {
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
- const { recordId, fieldDefinition } = useContext(FieldContext);
+ const { recordId, fieldDefinition, onMouseEnter, onMouseLeave, anchorId } =
+ useContext(FieldContext);
const shouldContainerBeClickable =
!readonly && !editModeContentOnly && !isInlineCellInEditMode;
@@ -114,12 +115,14 @@ export const RecordInlineCellContainer = () => {
if (!readonly) {
setIsFocused(true);
}
+ onMouseEnter?.();
};
const handleContainerMouseLeave = () => {
if (!readonly) {
setIsFocused(false);
}
+ onMouseLeave?.();
};
const theme = useTheme();
@@ -164,7 +167,7 @@ export const RecordInlineCellContainer = () => {
{isInlineCellInEditMode && (
)}
-
+
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellHoveredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellHoveredPortal.tsx
new file mode 100644
index 0000000000..b59f667996
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellHoveredPortal.tsx
@@ -0,0 +1,11 @@
+import styled from '@emotion/styled';
+
+const StyledRecordTableCellHoveredPortal = styled.div`
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+`;
+
+export const RecordInlineCellHoveredPortal = StyledRecordTableCellHoveredPortal;
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellHoveredPortalContent.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellHoveredPortalContent.tsx
new file mode 100644
index 0000000000..f166cf2491
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellHoveredPortalContent.tsx
@@ -0,0 +1,66 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+
+const StyledRecordTableCellHoveredPortalContent = styled.div<{
+ readonly?: boolean;
+ isCentered?: boolean;
+}>`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+ width: 100%;
+
+ ${({ isCentered }) =>
+ isCentered === true &&
+ `
+ justify-content: center;
+ `};
+
+ ${({ readonly }) =>
+ !readonly &&
+ css`
+ cursor: pointer;
+ `};
+`;
+
+const StyledInlineCellBaseContainer = styled.div<{ readonly: boolean }>`
+ box-sizing: border-box;
+ width: 100%;
+ display: flex;
+ height: fit-content;
+ gap: ${({ theme }) => theme.spacing(1)};
+ user-select: none;
+ align-items: center;
+ cursor: ${({ readonly }) => (readonly ? 'default' : 'pointer')};
+`;
+
+type RecordInlineCellHoveredPortalContentProps = {
+ children: React.ReactNode;
+ readonly: boolean;
+ isCentered?: boolean;
+ onClick?: () => void;
+ onMouseLeave?: () => void;
+};
+
+export const RecordInlineCellHoveredPortalContent = ({
+ children,
+ isCentered,
+ readonly,
+ onClick,
+ onMouseLeave,
+}: RecordInlineCellHoveredPortalContentProps) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
index d17c86ed28..49f0cdf45d 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx
@@ -1,12 +1,12 @@
-import groupBy from 'lodash.groupby';
-
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
-import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
-import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
+import { RecordFieldListCellHoveredPortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal';
+import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
+import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
+import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
@@ -19,11 +19,9 @@ import { useRecordShowContainerData } from '@/object-record/record-show/hooks/us
import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
-import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
-import { isDefined } from 'twenty-shared/utils';
-import { FieldMetadataType } from '~/generated-metadata/graphql';
+import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
type FieldsCardProps = {
objectNameSingular: string;
@@ -38,16 +36,14 @@ export const FieldsCard = ({
objectRecordId,
showDuplicatesSection = true,
}: FieldsCardProps) => {
- const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } =
- useRecordShowContainerData({
- objectNameSingular,
- objectRecordId,
- });
+ const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
+ objectRecordId,
+ });
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
- const { objectMetadataItems } = useObjectMetadataItems();
+
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
@@ -57,63 +53,34 @@ export const FieldsCard = ({
const { isInRightDrawer } = useIsInRightDrawerOrThrow();
- const availableFieldMetadataItems = objectMetadataItem.readableFields
- .filter(
- (fieldMetadataItem) =>
- isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
- fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
- )
- .sort((fieldMetadataItemA, fieldMetadataItemB) =>
- fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
- );
-
- const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
- availableFieldMetadataItems
- .filter(
- (fieldMetadataItem) =>
- fieldMetadataItem.name !== 'createdAt' &&
- fieldMetadataItem.name !== 'deletedAt',
- )
- .filter(
- (fieldMetadataItem) =>
- fieldMetadataItem.type !== FieldMetadataType.RICH_TEXT_V2,
- ),
- (fieldMetadataItem) =>
- fieldMetadataItem.type === FieldMetadataType.RELATION
- ? 'relationFieldMetadataItems'
- : 'inlineFieldMetadataItems',
- );
-
- const inlineRelationFieldMetadataItems = relationFieldMetadataItems?.filter(
- (fieldMetadataItem) =>
- (objectNameSingular === CoreObjectNameSingular.Note &&
- fieldMetadataItem.name === 'noteTargets') ||
- (objectNameSingular === CoreObjectNameSingular.Task &&
- fieldMetadataItem.name === 'taskTargets'),
- );
-
- const boxedRelationFieldMetadataItems = relationFieldMetadataItems?.filter(
- (fieldMetadataItem) =>
- !(
- (objectNameSingular === CoreObjectNameSingular.Note &&
- fieldMetadataItem.name === 'noteTargets') ||
- (objectNameSingular === CoreObjectNameSingular.Task &&
- fieldMetadataItem.name === 'taskTargets')
- ) &&
- isDefined(fieldMetadataItem.relation?.targetObjectMetadata.id) &&
- getObjectPermissionsForObject(
- objectPermissionsByObjectMetadataId,
- fieldMetadataItem.relation?.targetObjectMetadata.id,
- ).canReadObjectRecords,
- );
-
const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
});
+ const setRecordFieldListHoverPosition = useSetRecoilComponentState(
+ recordFieldListHoverPositionComponentState,
+ 'fields-card',
+ );
+
+ const handleMouseEnter = (index: number) => {
+ setRecordFieldListHoverPosition(index);
+ };
+
+ const {
+ inlineFieldMetadataItems,
+ inlineRelationFieldMetadataItems,
+ boxedRelationFieldMetadataItems,
+ } = useFieldListFieldMetadataItems({
+ objectNameSingular,
+ });
+
return (
- <>
+
{isPrefetchLoading ? (
@@ -200,6 +167,13 @@ export const FieldsCard = ({
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
+ onMouseEnter: () =>
+ handleMouseEnter(
+ index + (inlineRelationFieldMetadataItems?.length ?? 0),
+ ),
+ anchorId: `record-field-list-cell-${
+ index + (inlineRelationFieldMetadataItems?.length ?? 0)
+ }`,
}}
>
))}
- >
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
index 3400287bf4..04819d4421 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx
@@ -36,7 +36,6 @@ export const RecordShowContainer = ({
});
const { isPrefetchLoading, recordLoading } = useRecordShowContainerData({
- objectNameSingular,
objectRecordId,
});
diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx
index b0b9fca309..48dd0b709b 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx
@@ -1,4 +1,5 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
+import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
@@ -27,11 +28,9 @@ export const SummaryCard = ({
objectRecordId,
isInRightDrawer,
}: SummaryCardProps) => {
- const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } =
- useRecordShowContainerData({
- objectNameSingular,
- objectRecordId,
- });
+ const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
+ objectRecordId,
+ });
const recordCreatedAt = useRecoilValue(
recordStoreFamilySelector({
@@ -60,6 +59,11 @@ export const SummaryCard = ({
objectNameSingular,
});
+ const { labelIdentifierFieldMetadataItem } =
+ useLabelIdentifierFieldMetadataItem({
+ objectNameSingular,
+ });
+
const isTitleReadOnly = useIsRecordFieldReadOnly({
recordId: objectRecordId,
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts
index c1f951db8d..cc6694010c 100644
--- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts
+++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts
@@ -1,22 +1,14 @@
-import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
import { useRecoilState } from 'recoil';
type UseRecordShowContainerDataProps = {
- objectNameSingular: string;
objectRecordId: string;
};
export const useRecordShowContainerData = ({
- objectNameSingular,
objectRecordId,
}: UseRecordShowContainerDataProps) => {
- const { labelIdentifierFieldMetadataItem } =
- useLabelIdentifierFieldMetadataItem({
- objectNameSingular,
- });
-
const [recordLoading] = useRecoilState(
recordLoadingFamilyState(objectRecordId),
);
@@ -25,7 +17,6 @@ export const useRecordShowContainerData = ({
return {
recordLoading,
- labelIdentifierFieldMetadataItem,
isPrefetchLoading,
};
};
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
index 328c7c1884..d7ac9c91f0 100644
--- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx
@@ -9,7 +9,6 @@ import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/reco
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
-import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { computeRecordShowComponentInstanceId } from '@/object-record/record-show/utils/computeRecordShowComponentInstanceId';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { PageHeaderToggleCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderToggleCommandMenuButton';
@@ -17,6 +16,7 @@ import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
import { RecordShowPageTitle } from '~/pages/object-record/RecordShowPageTitle';
+import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
export const RecordShowPage = () => {
const parameters = useParams<{