Files
twenty/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx
T
Etienne d0c1841f0f File - Migrate avatarUrl > avatarFile on person (data migration + logic) + Attachment data migration (#17752)
- Migration command
    - Check IS_FILES_FIELD_MIGRATED:false
    - Check or create avatarFile field
    - Fetch all people with avatarUrl
           - Move (Copy/move) file in storage
           - Create core.file record
           - Update person record
           
    - bonus : attachment migration  : fullPath > file (same logic)
   
- BE logic
    - Add avatarFile field on person

- FE logic 
   - Adapt logic to upload on/display avatarFile data

The whole imageIdentifier logic will be done later
2026-02-11 15:07:05 +00:00

240 lines
11 KiB
TypeScript

import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
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 { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { FieldsWidgetCellEditModePortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal';
import { FieldsWidgetCellHoveredPortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal';
import { FieldsWidgetSectionContainer } from '@/page-layout/widgets/fields/components/FieldsWidgetSectionContainer';
import { useFieldsWidgetSectionsWithIndices } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices';
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 { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
`;
const StyledPropertyBox = styled.div`
align-self: stretch;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: ${({ theme }) => theme.spacing(3)};
`;
type FieldsWidgetProps = {
widget: PageLayoutWidget;
};
export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
const targetRecord = useTargetRecord();
const { isInRightDrawer } = useLayoutRenderingContext();
const instanceId = `${widget.id}-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`;
const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
objectRecordId: targetRecord.id,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId: targetRecord.id,
objectMetadataId: objectMetadataItem.id,
});
const setRecordFieldListHoverPosition = useSetRecoilComponentState(
recordFieldListHoverPositionComponentState,
instanceId,
);
const { sectionsWithFieldIndices } = useFieldsWidgetSectionsWithIndices(
targetRecord.targetObjectNameSingular,
);
const hasFieldsToDisplay = sectionsWithFieldIndices.length > 0;
if (!hasFieldsToDisplay) {
return (
<RightDrawerProvider value={{ isInRightDrawer }}>
<StyledContainer>
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{t`No fields to display`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{t`Configure this widget to display fields`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
</StyledContainer>
</RightDrawerProvider>
);
}
return (
<RecordFieldsScopeContextProvider value={{ scopeInstanceId: instanceId }}>
<StyledContainer>
<RecordFieldListComponentInstanceContext.Provider
value={{
instanceId,
}}
>
{sectionsWithFieldIndices.map((section) => (
<FieldsWidgetSectionContainer
key={section.id}
title={section.title}
>
<StyledPropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{section.fields.map(
({ field: fieldMetadataItem, globalIndex }) => {
const isActivityTarget = isActivityTargetField(
fieldMetadataItem.name,
targetRecord.targetObjectNameSingular,
);
return (
<FieldContext.Provider
key={targetRecord.id + fieldMetadataItem.id}
value={{
recordId: targetRecord.id,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition:
formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: globalIndex,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId(
{
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
},
),
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly:
fieldMetadataItem.isUIReadOnly ?? false,
},
}),
onMouseEnter: () =>
setRecordFieldListHoverPosition(globalIndex),
anchorId: `${getRecordFieldInputInstanceId({
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
})}`,
}}
>
{isActivityTarget ? (
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputInstanceId(
{
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
},
)}
activityObjectNameSingular={
targetRecord.targetObjectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={targetRecord.id}
showLabel={true}
maxWidth={200}
/>
) : (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<RecordInlineCell loading={recordLoading} />
</RecordFieldComponentInstanceContext.Provider>
)}
</FieldContext.Provider>
);
},
)}
</>
)}
</StyledPropertyBox>
</FieldsWidgetSectionContainer>
))}
<FieldsWidgetCellHoveredPortal
objectMetadataItem={objectMetadataItem}
recordId={targetRecord.id}
/>
<FieldsWidgetCellEditModePortal
objectMetadataItem={objectMetadataItem}
recordId={targetRecord.id}
/>
</RecordFieldListComponentInstanceContext.Provider>
</StyledContainer>
</RecordFieldsScopeContextProvider>
);
};