diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionFieldEdit.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionFieldEdit.tsx
new file mode 100644
index 0000000000..9588874497
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionFieldEdit.tsx
@@ -0,0 +1,126 @@
+import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
+import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
+import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
+import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
+import {
+ FieldContext,
+ type GenericFieldContextType,
+} from '@/object-record/record-field/ui/contexts/FieldContext';
+import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
+import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
+import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
+import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
+import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
+import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId';
+import { FieldWidgetEditAction } from '@/page-layout/widgets/field/components/FieldWidgetEditAction';
+import { FieldWidgetRelationEditAction } from '@/page-layout/widgets/field/components/FieldWidgetRelationEditAction';
+import { getFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils/getFieldWidgetInstanceId';
+import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
+import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
+import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
+import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
+import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
+import { useCurrentWidget } from '../hooks/useCurrentWidget';
+
+export const WidgetActionFieldEdit = () => {
+ const widget = useCurrentWidget();
+ const targetRecord = useTargetRecord();
+ const { isInRightDrawer } = useLayoutRenderingContext();
+
+ const { objectMetadataItem } = useObjectMetadataItem({
+ objectNameSingular: targetRecord.targetObjectNameSingular,
+ });
+
+ const fieldMetadataId = isFieldWidget(widget)
+ ? widget.configuration.fieldMetadataId
+ : undefined;
+
+ const resolvedFieldMetadataId = useResolveFieldMetadataIdFromNameOrId(
+ fieldMetadataId ?? '',
+ );
+
+ const { fieldMetadataItem } = useFieldMetadataItemById(
+ resolvedFieldMetadataId ?? '',
+ );
+
+ const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
+
+ const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ objectRecordId: targetRecord.id,
+ });
+
+ const isRecordReadOnly = useIsRecordReadOnly({
+ recordId: targetRecord.id,
+ objectMetadataId: objectMetadataItem.id,
+ });
+
+ assertIsDefinedOrThrow(fieldMetadataItem);
+
+ const fieldDefinition = formatFieldMetadataItemAsColumnDefinition({
+ field: fieldMetadataItem,
+ position: 0,
+ objectMetadataItem,
+ showLabel: true,
+ labelWidth: 90,
+ });
+
+ const isRelationField =
+ isFieldRelation(fieldDefinition) || isFieldMorphRelation(fieldDefinition);
+
+ if (isRelationField) {
+ return (
+
+ );
+ }
+
+ const instanceId = getFieldWidgetInstanceId({
+ recordId: targetRecord.id,
+ fieldName: fieldMetadataItem.name,
+ isInRightDrawer,
+ });
+
+ const recordFieldInputInstanceId = getRecordFieldInputInstanceId({
+ recordId: targetRecord.id,
+ fieldName: fieldMetadataItem.name,
+ prefix: instanceId,
+ });
+
+ const fieldContextValue = {
+ recordId: targetRecord.id,
+ maxWidth: 200,
+ isLabelIdentifier: false,
+ fieldDefinition,
+ useUpdateRecord: useUpdateOneObjectRecordMutation,
+ isDisplayModeFixHeight: false,
+ isRecordFieldReadOnly: isRecordFieldReadOnly({
+ isRecordReadOnly,
+ objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
+ objectPermissionsByObjectMetadataId,
+ objectMetadataId: objectMetadataItem.id,
+ }),
+ fieldMetadataItem: {
+ id: fieldMetadataItem.id,
+ isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
+ },
+ }),
+ anchorId: recordFieldInputInstanceId,
+ } satisfies GenericFieldContextType;
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionRenderer.tsx
index e8615b048e..fc979eb21a 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionRenderer.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetActionRenderer.tsx
@@ -1,16 +1,8 @@
-import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
-import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
-import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
-import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
-import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId';
-import { FieldWidgetRelationEditAction } from '@/page-layout/widgets/field/components/FieldWidgetRelationEditAction';
-import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
import { type WidgetAction } from '@/page-layout/widgets/types/WidgetAction';
-import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
-import { assertIsDefinedOrThrow, CustomError } from 'twenty-shared/utils';
+import { CustomError } from 'twenty-shared/utils';
import { WidgetType } from '~/generated/graphql';
import { useCurrentWidget } from '../hooks/useCurrentWidget';
+import { WidgetActionFieldEdit } from './WidgetActionFieldEdit';
type WidgetActionRendererProps = {
action: WidgetAction;
@@ -18,50 +10,9 @@ type WidgetActionRendererProps = {
export const WidgetActionRenderer = ({ action }: WidgetActionRendererProps) => {
const widget = useCurrentWidget();
- const targetRecord = useTargetRecord();
-
- const { objectMetadataItem } = useObjectMetadataItem({
- objectNameSingular: targetRecord.targetObjectNameSingular,
- });
-
- const fieldMetadataId = isFieldWidget(widget)
- ? widget.configuration.fieldMetadataId
- : undefined;
-
- const resolvedFieldMetadataId = useResolveFieldMetadataIdFromNameOrId(
- fieldMetadataId ?? '',
- );
-
- const { fieldMetadataItem } = useFieldMetadataItemById(
- resolvedFieldMetadataId ?? '',
- );
if (action.id === 'edit' && widget.type === WidgetType.FIELD) {
- assertIsDefinedOrThrow(fieldMetadataItem);
-
- const fieldDefinition = formatFieldMetadataItemAsColumnDefinition({
- field: fieldMetadataItem,
- position: 0,
- objectMetadataItem,
- showLabel: true,
- labelWidth: 90,
- });
-
- const isRelationField =
- isFieldRelation(fieldDefinition) || isFieldMorphRelation(fieldDefinition);
-
- if (!isRelationField) {
- throw new Error(
- 'Edit action is only available for relation fields for now',
- );
- }
-
- return (
-
- );
+ return ;
}
throw new CustomError(
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetDisplay.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetDisplay.tsx
index 2b35e599eb..54724bd751 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetDisplay.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetDisplay.tsx
@@ -12,6 +12,7 @@ import { FieldWidgetCellEditModePortal } from '@/page-layout/widgets/field/compo
import { FieldWidgetCellHoveredPortal } from '@/page-layout/widgets/field/components/FieldWidgetCellHoveredPortal';
import { FieldWidgetInlineCell } from '@/page-layout/widgets/field/components/FieldWidgetInlineCell';
import { fieldWidgetHoverComponentState } from '@/page-layout/widgets/field/states/fieldWidgetHoverComponentState';
+import { getFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils/getFieldWidgetInstanceId';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
@@ -41,7 +42,11 @@ export const FieldWidgetDisplay = ({
fieldWidgetHoverComponentState,
);
- const instanceId = `field-widget-${recordId}-${fieldMetadataItem.name}-${isInRightDrawer ? 'right-drawer' : ''}`;
+ const instanceId = getFieldWidgetInstanceId({
+ recordId,
+ fieldName: fieldMetadataItem.name,
+ isInRightDrawer,
+ });
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetEditAction.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetEditAction.tsx
new file mode 100644
index 0000000000..058fbfcb2a
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetEditAction.tsx
@@ -0,0 +1,44 @@
+import { useContext } from 'react';
+
+import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
+import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
+import { useOpenFieldWidgetFieldInputEditMode } from '@/page-layout/widgets/field/hooks/useOpenFieldWidgetFieldInputEditMode';
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { IconPencil } from 'twenty-ui/display';
+import { LightIconButton } from 'twenty-ui/input';
+
+const StyledEditButton = styled(LightIconButton)`
+ ${({ theme }) => css`
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity ${theme.animation.duration.instant}s ease;
+ `}
+
+ .widget:hover & {
+ opacity: 1;
+ pointer-events: auto;
+ }
+`;
+
+export const FieldWidgetEditAction = () => {
+ const { recordId, fieldDefinition } = useContext(FieldContext);
+ const { openInlineCell } = useInlineCell();
+ const { openFieldInput } = useOpenFieldWidgetFieldInputEditMode();
+
+ const handleClick = () => {
+ openInlineCell();
+ openFieldInput({
+ fieldDefinition,
+ recordId,
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationEditAction.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationEditAction.tsx
index 0452fad100..e8668df6dd 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationEditAction.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidgetRelationEditAction.tsx
@@ -101,7 +101,6 @@ export const FieldWidgetRelationEditAction = ({
isDropdownOpen={isRelationSelectionDropdownOpen}
Icon={IconPencil}
accent="secondary"
- className="display-on-widget-hover"
/>
);
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/utils/getFieldWidgetInstanceId.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/getFieldWidgetInstanceId.ts
new file mode 100644
index 0000000000..2b7f66f760
--- /dev/null
+++ b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/getFieldWidgetInstanceId.ts
@@ -0,0 +1,11 @@
+export const getFieldWidgetInstanceId = ({
+ recordId,
+ fieldName,
+ isInRightDrawer,
+}: {
+ recordId: string;
+ fieldName: string;
+ isInRightDrawer: boolean;
+}): string => {
+ return `field-widget-${recordId}-${fieldName}${isInRightDrawer ? '-right-drawer' : ''}`;
+};
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetActions.ts b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetActions.ts
index f2a7884dc3..b9926b316a 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetActions.ts
+++ b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetActions.ts
@@ -1,12 +1,13 @@
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
-import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
-import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
+import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
+import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
+import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
import { type WidgetAction } from '@/page-layout/widgets/types/WidgetAction';
+import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { isDefined } from 'twenty-shared/utils';
@@ -35,6 +36,13 @@ export const useWidgetActions = ({
resolvedFieldMetadataId ?? '',
);
+ const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
+
+ const isRecordReadOnly = useIsRecordReadOnly({
+ recordId: targetRecord.id,
+ objectMetadataId: objectMetadataItem.id,
+ });
+
const actions: WidgetAction[] = [];
if (
@@ -45,18 +53,19 @@ export const useWidgetActions = ({
return actions;
}
- const fieldDefinition = formatFieldMetadataItemAsColumnDefinition({
- field: fieldMetadataItem,
- position: 0,
- objectMetadataItem,
- showLabel: true,
- labelWidth: 90,
+ const isFieldReadOnly = isRecordFieldReadOnly({
+ isRecordReadOnly,
+ objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
+ objectPermissionsByObjectMetadataId,
+ objectMetadataId: objectMetadataItem.id,
+ }),
+ fieldMetadataItem: {
+ id: fieldMetadataItem.id,
+ isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
+ },
});
- const isRelationField =
- isFieldRelation(fieldDefinition) || isFieldMorphRelation(fieldDefinition);
-
- if (isRelationField) {
+ if (!isFieldReadOnly) {
actions.push({
id: 'edit',
position: 0,