Edit simple values in Field widget (#16749)

https://github.com/user-attachments/assets/116b8259-b366-47bf-8068-b5276b138e03
This commit is contained in:
Baptiste Devessier
2025-12-22 15:21:39 +01:00
committed by GitHub
parent bb73cbc380
commit 1324ad1ee3
7 changed files with 212 additions and 67 deletions
@@ -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 (
<FieldWidgetRelationEditAction
fieldDefinition={fieldDefinition}
recordId={targetRecord.id}
/>
);
}
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 (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: recordFieldInputInstanceId,
}}
>
<FieldContext.Provider value={fieldContextValue}>
<FieldWidgetEditAction />
</FieldContext.Provider>
</RecordFieldComponentInstanceContext.Provider>
);
};
@@ -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 (
<FieldWidgetRelationEditAction
fieldDefinition={fieldDefinition}
recordId={targetRecord.id}
/>
);
return <WidgetActionFieldEdit />;
}
throw new CustomError(
@@ -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();
@@ -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 (
<StyledEditButton
Icon={IconPencil}
accent="secondary"
onClick={handleClick}
/>
);
};
@@ -101,7 +101,6 @@ export const FieldWidgetRelationEditAction = ({
isDropdownOpen={isRelationSelectionDropdownOpen}
Icon={IconPencil}
accent="secondary"
className="display-on-widget-hover"
/>
);
@@ -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' : ''}`;
};
@@ -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,