Add field widget (#16609)

- Ability to display the details of a field
- The field can be edited (relations edition will be supported later)
- For now, the widget configuration stores the name of the field instead
of its fieldMetadataId. A hook resolves the fieldMetadataId from the
list of fields and the provided name. This will be replaced once we
migrate the configuration to the backend.

## Demo


https://github.com/user-attachments/assets/ab7efbda-66b2-46c1-b641-c350977c31dd

## Remaining to do

- Edition of relations
This commit is contained in:
Baptiste Devessier
2025-12-18 18:25:51 +01:00
committed by GitHub
parent 79a03b8041
commit b5ebc44fee
35 changed files with 2465 additions and 13 deletions
@@ -0,0 +1,122 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
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 } from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { FieldWidgetCellEditModePortal } from '@/page-layout/widgets/field/components/FieldWidgetCellEditModePortal';
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 { 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';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
padding: ${({ theme }) => theme.spacing(1)};
width: 100%;
`;
type FieldWidgetDisplayProps = {
fieldDefinition: FieldDefinition<any>;
fieldMetadataItem: FieldMetadataItem;
objectMetadataItem: ObjectMetadataItem;
recordId: string;
isInRightDrawer: boolean;
};
export const FieldWidgetDisplay = ({
fieldDefinition,
fieldMetadataItem,
objectMetadataItem,
recordId,
isInRightDrawer,
}: FieldWidgetDisplayProps) => {
const [isHovered, setIsHovered] = useRecoilComponentState(
fieldWidgetHoverComponentState,
);
const instanceId = `field-widget-${recordId}-${fieldMetadataItem.name}-${isInRightDrawer ? 'right-drawer' : ''}`;
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular: objectMetadataItem.nameSingular,
objectRecordId: recordId,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId: objectMetadataItem.id,
});
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<StyledContainer>
<FieldContext.Provider
value={{
recordId,
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,
},
}),
onMouseEnter: handleMouseEnter,
anchorId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<FieldWidgetInlineCell instanceIdPrefix={instanceId} />
</FieldContext.Provider>
</StyledContainer>
<FieldWidgetCellHoveredPortal
objectMetadataItem={objectMetadataItem}
fieldMetadataItem={fieldMetadataItem}
recordId={recordId}
instanceId={instanceId}
isHovered={isHovered}
onMouseLeave={handleMouseLeave}
/>
<FieldWidgetCellEditModePortal
objectMetadataItem={objectMetadataItem}
fieldMetadataItem={fieldMetadataItem}
recordId={recordId}
instanceId={instanceId}
/>
</RecordFieldComponentInstanceContext.Provider>
</RightDrawerProvider>
);
};