Refactor inline-cell inputs on board (#13921)

Refactor FieldsCard (aka RecordFieldList) and RecordDetailSection so
they re-use the same components
This commit is contained in:
Charles Bochet
2025-08-14 14:52:38 +02:00
committed by GitHub
parent 9ea0662936
commit 8c78314b52
28 changed files with 450 additions and 444 deletions
@@ -4,8 +4,10 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { RecordFieldListCellEditModePortalContent } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellEditModePortalContent';
import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider';
import { useFieldListFieldMetadataFromPosition } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition';
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListCellEditModePositionComponentState } from '@/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState';
import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
type RecordFieldListCellEditModePortalProps = {
@@ -17,6 +19,10 @@ export const RecordFieldListCellEditModePortal = ({
objectMetadataItem,
recordId,
}: RecordFieldListCellEditModePortalProps) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldListComponentInstanceContext,
);
const editModePosition = useRecoilComponentValue(
recordFieldListCellEditModePositionComponentState,
);
@@ -35,7 +41,7 @@ export const RecordFieldListCellEditModePortal = ({
fieldMetadataItem={editedFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
anchorIdPrefix={'record-field-list-inline-cell'}
anchorIdPrefix={instanceId}
>
<RecordFieldListInputContextProvider>
<RecordFieldListCellEditModePortalContent />
@@ -4,8 +4,10 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { RecordFieldListCellHoveredPortalContent } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent';
import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider';
import { useFieldListFieldMetadataFromPosition } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataFromPosition';
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { isDefined } from 'twenty-shared/utils';
type RecordFieldListCellHoveredPortalProps = {
@@ -17,6 +19,10 @@ export const RecordFieldListCellHoveredPortal = ({
objectMetadataItem,
recordId,
}: RecordFieldListCellHoveredPortalProps) => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldListComponentInstanceContext,
);
const hoverPosition = useRecoilComponentValue(
recordFieldListHoverPositionComponentState,
);
@@ -35,7 +41,7 @@ export const RecordFieldListCellHoveredPortal = ({
fieldMetadataItem={hoveredFieldMetadataItem}
objectMetadataItem={objectMetadataItem}
recordId={recordId}
anchorIdPrefix={'record-field-list-inline-cell'}
anchorIdPrefix={instanceId}
>
<RecordFieldListInputContextProvider>
<RecordFieldListCellHoveredPortalContent />
@@ -1,3 +1,4 @@
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListCellEditModePositionComponentState } from '@/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
@@ -8,16 +9,28 @@ import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/c
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 { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useContext } from 'react';
export const RecordFieldListCellHoveredPortalContent = () => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
RecordFieldListComponentInstanceContext,
);
const { editModeContentOnly, isCentered } = useRecordInlineCellContext();
const { isRecordFieldReadOnly, recordId, fieldDefinition } =
useContext(FieldContext);
const { openInlineCell } = useInlineCell();
const { openInlineCell } = useInlineCell(
getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix: instanceId,
}),
);
const shouldContainerBeClickable =
!isRecordFieldReadOnly && !editModeContentOnly;
@@ -38,7 +51,7 @@ export const RecordFieldListCellHoveredPortalContent = () => {
openFieldInput({
fieldDefinition,
recordId,
prefix: 'inline-cell',
prefix: instanceId,
});
}
};
@@ -51,10 +64,9 @@ export const RecordFieldListCellHoveredPortalContent = () => {
<RecordInlineCellHoveredPortalContent
readonly={isRecordFieldReadOnly}
isCentered={isCentered}
onClick={handleClick}
onMouseLeave={handleMouseLeave}
>
<RecordInlineCellDisplayMode isHovered={true}>
<RecordInlineCellDisplayMode isHovered={true} onClick={handleClick}>
{editModeContentOnly ? <FieldInput /> : <FieldDisplay />}
</RecordInlineCellDisplayMode>
</RecordInlineCellHoveredPortalContent>
@@ -0,0 +1,254 @@
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 { RecordFieldListCellEditModePortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellEditModePortal';
import { RecordFieldListCellHoveredPortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
import { RecordDetailDuplicatesSection } from '@/object-record/record-field-list/record-detail-section/duplicate/components/RecordDetailDuplicatesSection';
import { RecordDetailRelationSection } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection';
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';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
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 { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
type RecordFieldListProps = {
instanceId: string;
objectNameSingular: string;
objectRecordId: string;
showDuplicatesSection?: boolean;
showRelationSections?: boolean;
excludeFieldMetadataIds?: string[];
excludeCreatedAtAndUpdatedAt?: boolean;
};
export const RecordFieldList = ({
instanceId,
objectNameSingular,
objectRecordId,
showDuplicatesSection = true,
showRelationSections = true,
excludeFieldMetadataIds = [],
excludeCreatedAtAndUpdatedAt = true,
}: RecordFieldListProps) => {
const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
objectRecordId,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
});
const setRecordFieldListHoverPosition = useSetRecoilComponentState(
recordFieldListHoverPositionComponentState,
instanceId,
);
const handleMouseEnter = (index: number) => {
setRecordFieldListHoverPosition(index);
};
const {
inlineFieldMetadataItems,
inlineRelationFieldMetadataItems,
boxedRelationFieldMetadataItems,
} = useFieldListFieldMetadataItems({
objectNameSingular,
excludeFieldMetadataIds,
showRelationSections,
excludeCreatedAtAndUpdatedAt,
});
return (
<RecordFieldListComponentInstanceContext.Provider
value={{
instanceId,
}}
>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{inlineRelationFieldMetadataItems?.map(
(fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
onMouseEnter: () => handleMouseEnter(index),
anchorId: `${instanceId}-${index}`,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
}}
>
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputInstanceId({
recordId: objectRecordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
})}
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={objectRecordId}
showLabel={true}
maxWidth={200}
/>
</FieldContext.Provider>
),
)}
{inlineFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
onMouseEnter: () =>
handleMouseEnter(
index + (inlineRelationFieldMetadataItems?.length ?? 0),
),
anchorId: `${instanceId}-${
index + (inlineRelationFieldMetadataItems?.length ?? 0)
}`,
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: objectRecordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<RecordInlineCell
loading={recordLoading}
instanceIdPrefix={instanceId}
/>
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
))}
</>
)}
</PropertyBox>
{showDuplicatesSection && (
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
)}
{boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || recordLoading}
/>
</FieldContext.Provider>
))}
<RecordFieldListCellHoveredPortal
objectMetadataItem={objectMetadataItem}
recordId={objectRecordId}
/>
<RecordFieldListCellEditModePortal
objectMetadataItem={objectMetadataItem}
recordId={objectRecordId}
/>
</RecordFieldListComponentInstanceContext.Provider>
);
};
@@ -11,10 +11,16 @@ import { isDefined } from 'twenty-shared/utils';
type UseFieldListFieldMetadataItemsProps = {
objectNameSingular: string;
excludeFieldMetadataIds?: string[];
excludeCreatedAtAndUpdatedAt?: boolean;
showRelationSections?: boolean;
};
export const useFieldListFieldMetadataItems = ({
objectNameSingular,
excludeFieldMetadataIds = [],
showRelationSections = true,
excludeCreatedAtAndUpdatedAt = true,
}: UseFieldListFieldMetadataItemsProps) => {
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
@@ -33,7 +39,13 @@ export const useFieldListFieldMetadataItems = ({
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id &&
!excludeFieldMetadataIds.includes(fieldMetadataItem.id) &&
(!excludeCreatedAtAndUpdatedAt ||
(fieldMetadataItem.name !== 'createdAt' &&
fieldMetadataItem.name !== 'deletedAt')) &&
(showRelationSections ||
fieldMetadataItem.type !== FieldMetadataType.RELATION),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
@@ -4,4 +4,4 @@ const StyledRecordsList = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
`;
export { StyledRecordsList as RecordDetailRecordsList };
export { StyledRecordsList as RecordDetailRecordsListContainer };
@@ -8,4 +8,4 @@ const StyledListItem = styled.div`
height: ${({ theme }) => theme.spacing(10)};
`;
export { StyledListItem as RecordDetailRecordsListItem };
export { StyledListItem as RecordDetailRecordsListItemContainer };
@@ -1,6 +1,14 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Section } from 'twenty-ui/layout';
const StyledRecordDetailSectionContainer = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: ${({ theme }) => theme.spacing(3)};
width: auto;
`;
const StyledHeader = styled.header<{
isDropdownOpen?: boolean;
@@ -34,7 +42,8 @@ const StyledLink = styled(Link)`
}
`;
type RecordDetailSectionHeaderProps = {
type RecordDetailSectionContainerProps = {
children: React.ReactNode;
title: string;
link?: { to: string; label: string };
rightAdornment?: React.ReactNode;
@@ -42,28 +51,31 @@ type RecordDetailSectionHeaderProps = {
areRecordsAvailable?: boolean;
};
export const RecordDetailSectionHeader = ({
export const RecordDetailSectionContainer = ({
children,
title,
link,
rightAdornment,
hideRightAdornmentOnMouseLeave = true,
areRecordsAvailable = false,
}: RecordDetailSectionHeaderProps) => {
}: RecordDetailSectionContainerProps) => {
const [isHovered, setIsHovered] = useState(false);
return (
<StyledHeader
areRecordsAvailable={areRecordsAvailable}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<StyledTitle>
<StyledTitleLabel>{title}</StyledTitleLabel>
{link && <StyledLink to={link.to}>{link.label}</StyledLink>}
</StyledTitle>
{hideRightAdornmentOnMouseLeave && !isHovered && areRecordsAvailable
? null
: rightAdornment}
</StyledHeader>
<StyledRecordDetailSectionContainer>
<StyledHeader
areRecordsAvailable={areRecordsAvailable}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<StyledTitle>
<StyledTitleLabel>{title}</StyledTitleLabel>
{link && <StyledLink to={link.to}>{link.label}</StyledLink>}
</StyledTitle>
{hideRightAdornmentOnMouseLeave && !isHovered && areRecordsAvailable
? null
: rightAdornment}
</StyledHeader>
{children}
</StyledRecordDetailSectionContainer>
);
};
@@ -2,10 +2,9 @@ import { useOpenMergeRecordsPageInCommandMenu } from '@/command-menu/hooks/useOp
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordChip } from '@/object-record/components/RecordChip';
import { useFindDuplicateRecords } from '@/object-record/hooks/useFindDuplicateRecords';
import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList';
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
import { RecordDetailRecordsListContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListContainer';
import { RecordDetailRecordsListItemContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListItemContainer';
import { RecordDetailSectionContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailSectionContainer';
import { isDefined } from 'twenty-shared/utils';
import { IconArrowMerge } from 'twenty-ui/display';
@@ -44,28 +43,27 @@ export const RecordDetailDuplicatesSection = ({
return null;
return (
<RecordDetailSection>
<RecordDetailSectionHeader
title="Duplicates"
rightAdornment={
<LightIconButton
className="displayOnHover"
Icon={IconArrowMerge}
accent="tertiary"
onClick={openMergeRecordsPageInCommandMenu}
/>
}
/>
<RecordDetailRecordsList>
<RecordDetailSectionContainer
title="Duplicates"
rightAdornment={
<LightIconButton
className="displayOnHover"
Icon={IconArrowMerge}
accent="tertiary"
onClick={openMergeRecordsPageInCommandMenu}
/>
}
>
<RecordDetailRecordsListContainer>
{queryResults[0].slice(0, 5).map((duplicateRecord) => (
<RecordDetailRecordsListItem key={duplicateRecord.id}>
<RecordDetailRecordsListItemContainer key={duplicateRecord.id}>
<RecordChip
record={duplicateRecord}
objectNameSingular={objectNameSingular}
/>
</RecordDetailRecordsListItem>
</RecordDetailRecordsListItemContainer>
))}
</RecordDetailRecordsList>
</RecordDetailSection>
</RecordDetailRecordsListContainer>
</RecordDetailSectionContainer>
);
};
@@ -7,8 +7,8 @@ import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { RecordDetailDuplicatesSection } from '../RecordDetailDuplicatesSection';
import { ComponentDecorator } from 'twenty-ui/testing';
import { RecordDetailDuplicatesSection } from '../../components/RecordDetailDuplicatesSection';
const companiesMock = getCompaniesMock();
@@ -1,8 +1,8 @@
import { Fragment, useState } from 'react';
import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList';
import { RecordDetailRelationRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem';
import { RecordDetailRelationRecordsListItemEffect } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItemEffect';
import { RecordDetailRecordsListContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListContainer';
import { RecordDetailRelationRecordsListItem } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem';
import { RecordDetailRelationRecordsListItemEffect } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItemEffect';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
type RecordDetailRelationRecordsListProps = {
@@ -18,7 +18,7 @@ export const RecordDetailRelationRecordsList = ({
setExpandedItem(recordId === expandedItem ? '' : recordId);
return (
<RecordDetailRecordsList>
<RecordDetailRecordsListContainer>
{relationRecords.slice(0, 5).map((relationRecord) => (
<Fragment key={relationRecord.id}>
<RecordDetailRelationRecordsListItemEffect
@@ -33,6 +33,6 @@ export const RecordDetailRelationRecordsList = ({
/>
</Fragment>
))}
</RecordDetailRecordsList>
</RecordDetailRecordsListContainer>
);
};
@@ -4,33 +4,21 @@ import { motion } from 'framer-motion';
import { useCallback, useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { RecordChip } from '@/object-record/components/RecordChip';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
FieldContext,
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList';
import { RecordDetailRecordsListItemContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListItemContainer';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
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';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -38,6 +26,7 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { createPortal } from 'react-dom';
@@ -53,7 +42,7 @@ import { MenuItem } from 'twenty-ui/navigation';
import { AnimatedEaseInOut } from 'twenty-ui/utilities';
import { RelationType } from '~/generated-metadata/graphql';
const StyledListItem = styled(RecordDetailRecordsListItem)<{
const StyledListItem = styled(RecordDetailRecordsListItemContainer)<{
isDropdownOpen?: boolean;
}>`
${({ isDropdownOpen, theme }) =>
@@ -74,14 +63,6 @@ const StyledListItem = styled(RecordDetailRecordsListItem)<{
}
`;
const StyledPropertyBox = styled(PropertyBox)`
align-items: flex-start;
display: flex;
padding-left: ${({ theme }) => theme.spacing(0)};
padding-top: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(0)};
`;
const StyledClickableZone = styled.div`
align-items: center;
cursor: pointer;
@@ -133,8 +114,6 @@ export const RecordDetailRelationRecordsListItem = ({
relationObjectMetadataNameSingular,
);
const { objectMetadataItems } = useObjectMetadataItems();
const relationObjectPermissions = useObjectPermissionsForObject(
relationObjectMetadataItem.id,
);
@@ -150,18 +129,6 @@ export const RecordDetailRelationRecordsListItem = ({
relationObjectMetadataNameSingular ===
CoreObjectNameSingular.WorkspaceMember;
const availableRelationFieldMetadataItems = relationObjectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !==
relationObjectMetadataItem.labelIdentifierFieldMetadataId &&
fieldMetadataItem.id !== relationFieldMetadataId &&
fieldMetadataItem.name !== 'createdAt' &&
fieldMetadataItem.name !== 'deletedAt',
)
.sort();
const dropdownInstanceId = `record-field-card-menu-${relationFieldMetadataId}-${relationRecord.id}`;
const { closeDropdown } = useCloseDropdown();
@@ -213,17 +180,6 @@ export const RecordDetailRelationRecordsListItem = ({
await deleteOneRelationRecord(relationRecord.id);
};
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRelationRecord?.({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return [updateEntity, { loading: false }];
};
const handleClick = () => onClick(relationRecord.id);
const AnimatedIconChevronDown = useCallback<IconComponent>(
@@ -240,10 +196,7 @@ export const RecordDetailRelationRecordsListItem = ({
[isExpanded],
);
const isRelationRecordReadOnly = useIsRecordReadOnly({
recordId: relationRecord.id,
objectMetadataId: relationObjectMetadataItem.id,
});
const { isInRightDrawer } = useIsInRightDrawerOrThrow();
return (
<>
@@ -294,49 +247,14 @@ export const RecordDetailRelationRecordsListItem = ({
)}
</StyledListItem>
<AnimatedEaseInOut isOpen={isExpanded}>
<StyledPropertyBox>
{availableRelationFieldMetadataItems.map(
(fieldMetadataItem, index) => (
<FieldContext.Provider
key={fieldMetadataItem.id}
value={{
recordId: relationRecord.id,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem: relationObjectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRelationRecordReadOnly,
objectPermissions: relationObjectPermissions,
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular: relationObjectMetadataNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: relationObjectMetadataItem.isCustom,
}),
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: relationRecord.id,
fieldName: fieldMetadataItem.name,
prefix: 'record-detail',
}),
}}
>
<RecordInlineCell instanceIdPrefix="record-detail" />
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
),
)}
</StyledPropertyBox>
<RecordFieldList
instanceId={`record-detail-relation-${relationRecord.id}-${isInRightDrawer ? 'right-drawer' : ''}`}
objectNameSingular={relationObjectMetadataNameSingular}
objectRecordId={relationRecord.id}
showDuplicatesSection={false}
excludeCreatedAtAndUpdatedAt={true}
excludeFieldMetadataIds={[relationFieldMetadataId]}
/>
</AnimatedEaseInOut>
{createPortal(
<ConfirmationModal
@@ -5,6 +5,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { RecordDetailSectionContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailSectionContainer';
import { RecordDetailRelationRecordsList } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsList';
import { RecordDetailRelationSectionDropdown } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdown';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import {
FieldInputEventContext,
@@ -12,10 +15,6 @@ import {
} from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { usePersistField } from '@/object-record/record-field/ui/hooks/usePersistField';
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
import { RecordDetailRelationSectionDropdown } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdown';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
@@ -157,30 +156,29 @@ export const RecordDetailRelationSection = ({
onSubmit: handleSubmit,
}}
>
<RecordDetailSection>
<RecordDetailSectionHeader
title={fieldDefinition.label}
link={
isToManyObjects
? {
to: filterLinkHref,
label:
relationRecordsCount > 0
? t`All (${relationRecordsCount})`
: '',
}
: undefined
}
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0}
rightAdornment={
<RecordDetailRelationSectionDropdown loading={loading} />
}
/>
<RecordDetailSectionContainer
title={fieldDefinition.label}
link={
isToManyObjects
? {
to: filterLinkHref,
label:
relationRecordsCount > 0
? t`All (${relationRecordsCount})`
: '',
}
: undefined
}
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0}
rightAdornment={
<RecordDetailRelationSectionDropdown loading={loading} />
}
>
{relationRecords.length > 0 && (
<RecordDetailRelationRecordsList relationRecords={relationRecords} />
)}
</RecordDetailSection>
</RecordDetailSectionContainer>
</FieldInputEventContext.Provider>
);
};
@@ -1,11 +1,11 @@
import { useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordDetailRelationSectionDropdownToMany } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany';
import { RecordDetailRelationSectionDropdownToOne } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RecordDetailRelationSectionDropdownToMany } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany';
import { RecordDetailRelationSectionDropdownToOne } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne';
import { RelationType } from '~/generated-metadata/graphql';
type RecordDetailRelationSectionDropdownProps = {
@@ -12,6 +12,7 @@ import { getCompaniesMock } from '~/testing/mock-data/companies';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RightDrawerDecorator } from '~/testing/decorators/RightDrawerDecorator';
import { allMockPersonRecords } from '~/testing/mock-data/people';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { RecordDetailRelationSection } from '../RecordDetailRelationSection';
@@ -52,6 +53,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
</FieldContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
),
RightDrawerDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
@@ -18,6 +18,8 @@ export const FieldContextProvider = ({
objectRecordId,
customUseUpdateOneObjectHook,
overridenIsFieldEmpty,
onMouseEnter,
anchorId,
children,
}: {
clearable?: boolean;
@@ -28,6 +30,8 @@ export const FieldContextProvider = ({
objectRecordId: string;
customUseUpdateOneObjectHook?: RecordUpdateHook;
overridenIsFieldEmpty?: boolean;
onMouseEnter?: () => void;
anchorId?: string;
children: ReactNode;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
@@ -81,6 +85,8 @@ export const FieldContextProvider = ({
clearable,
overridenIsFieldEmpty,
isRecordFieldReadOnly,
onMouseEnter,
anchorId,
}}
>
{children}
@@ -91,7 +91,7 @@ export const RecordInlineCellAnchoredPortal = ({
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldMetadataItem.name,
prefix: 'inline-cell',
prefix: anchorIdPrefix,
}),
}}
>
@@ -64,8 +64,12 @@ const StyledEmptyField = styled.div`
export const RecordInlineCellDisplayMode = ({
children,
onClick,
isHovered,
}: React.PropsWithChildren<{ isHovered: boolean }>) => {
}: React.PropsWithChildren<{
isHovered: boolean;
onClick?: () => void;
}>) => {
const { t } = useLingui();
const { editModeContentOnly, showLabel, label, buttonIcon } =
@@ -86,7 +90,10 @@ export const RecordInlineCellDisplayMode = ({
return (
<>
<StyledRecordInlineCellNormalModeOuterContainer isHovered={isHovered}>
<StyledRecordInlineCellNormalModeOuterContainer
isHovered={isHovered}
onClick={onClick}
>
<StyledRecordInlineCellNormalModeInnerContainer>
{(isDisplayModeContentEmpty && !shouldDisplayEditModeOnFocus) ||
!children ? (
@@ -96,7 +103,9 @@ export const RecordInlineCellDisplayMode = ({
)}
</StyledRecordInlineCellNormalModeInnerContainer>
</StyledRecordInlineCellNormalModeOuterContainer>
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
{showEditButton && (
<RecordInlineCellButton Icon={buttonIcon} onClick={onClick} />
)}
</>
);
};
@@ -8,10 +8,16 @@ const StyledInlineCellButtonContainer = styled.div`
display: flex;
`;
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
export const RecordInlineCellButton = ({
Icon,
onClick,
}: {
Icon: IconComponent;
onClick?: () => void;
}) => {
return (
<AnimatedContainer>
<StyledInlineCellButtonContainer>
<StyledInlineCellButtonContainer onClick={onClick}>
<FloatingIconButton
size="small"
Icon={Icon}
@@ -1,5 +1,4 @@
import { RecordInlineCellHoveredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellHoveredPortal';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
const StyledRecordTableCellHoveredPortalContent = styled.div<{
@@ -16,12 +15,6 @@ const StyledRecordTableCellHoveredPortalContent = styled.div<{
`
justify-content: center;
`};
${({ readonly }) =>
!readonly &&
css`
cursor: pointer;
`};
`;
const StyledInlineCellBaseContainer = styled.div<{ readonly: boolean }>`
@@ -32,14 +25,12 @@ const StyledInlineCellBaseContainer = styled.div<{ readonly: boolean }>`
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;
};
@@ -47,14 +38,12 @@ export const RecordInlineCellHoveredPortalContent = ({
children,
isCentered,
readonly,
onClick,
onMouseLeave,
}: RecordInlineCellHoveredPortalContentProps) => {
return (
<RecordInlineCellHoveredPortal>
<StyledInlineCellBaseContainer
readonly={readonly}
onClick={onClick}
onMouseLeave={onMouseLeave}
>
<StyledRecordTableCellHoveredPortalContent
@@ -36,12 +36,10 @@ export const RecordInlineCellValue = () => {
}
return (
<>
<StyledClickableContainer readonly={readonly} isCentered={isCentered}>
<RecordInlineCellDisplayMode isHovered={false}>
<FieldDisplay />
</RecordInlineCellDisplayMode>
</StyledClickableContainer>
</>
<StyledClickableContainer readonly={readonly} isCentered={isCentered}>
<RecordInlineCellDisplayMode isHovered={false}>
<FieldDisplay />
</RecordInlineCellDisplayMode>
</StyledClickableContainer>
);
};
@@ -11,8 +11,8 @@ export const StyledPropertyBoxContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: ${({ theme }) => theme.spacing(3)};
`;
export const PropertyBox = ({ children, className }: PropertyBoxProps) => (
@@ -1,28 +1,11 @@
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 { RecordFieldListCellEditModePortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellEditModePortal';
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';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
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 { 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 { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList';
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import styled from '@emotion/styled';
const StyledPropertyBoxContainer = styled.div`
padding: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
type FieldsCardProps = {
objectNameSingular: string;
@@ -30,219 +13,21 @@ type FieldsCardProps = {
showDuplicatesSection?: boolean;
};
const INPUT_ID_PREFIX = 'fields-card';
export const FieldsCard = ({
objectNameSingular,
objectRecordId,
showDuplicatesSection = true,
}: FieldsCardProps) => {
const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
objectRecordId,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular,
objectRecordId,
});
const { isInRightDrawer } = useIsInRightDrawerOrThrow();
const isRecordReadOnly = useIsRecordReadOnly({
recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
});
const setRecordFieldListHoverPosition = useSetRecoilComponentState(
recordFieldListHoverPositionComponentState,
`fields-list-${objectRecordId}`,
);
const handleMouseEnter = (index: number) => {
setRecordFieldListHoverPosition(index);
};
const {
inlineFieldMetadataItems,
inlineRelationFieldMetadataItems,
boxedRelationFieldMetadataItems,
} = useFieldListFieldMetadataItems({
objectNameSingular,
});
return (
<RecordFieldListComponentInstanceContext.Provider
value={{
instanceId: `fields-list-${objectRecordId}`,
}}
>
<PropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{inlineRelationFieldMetadataItems?.map(
(fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
}}
>
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputInstanceId({
recordId: objectRecordId,
fieldName: fieldMetadataItem.name,
prefix: isInRightDrawer
? 'right-drawer-fields-card'
: 'fields-card',
})}
activityObjectNameSingular={
objectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={objectRecordId}
showLabel={true}
maxWidth={200}
/>
</FieldContext.Provider>
),
)}
{inlineFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
onMouseEnter: () =>
handleMouseEnter(
index + (inlineRelationFieldMetadataItems?.length ?? 0),
),
anchorId: `record-field-list-inline-cell-${
index + (inlineRelationFieldMetadataItems?.length ?? 0)
}`,
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: objectRecordId,
fieldName: fieldMetadataItem.name,
prefix: INPUT_ID_PREFIX,
}),
}}
>
<RecordInlineCell
loading={recordLoading}
instanceIdPrefix={INPUT_ID_PREFIX}
/>
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
))}
</>
)}
</PropertyBox>
{showDuplicatesSection && (
<RecordDetailDuplicatesSection
objectRecordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
)}
{boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
value={{
recordId: objectRecordId,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: index,
objectMetadataItem,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
}}
>
<RecordDetailRelationSection
loading={isPrefetchLoading || recordLoading}
/>
</FieldContext.Provider>
))}
<RecordFieldListCellHoveredPortal
objectMetadataItem={objectMetadataItem}
recordId={objectRecordId}
<StyledPropertyBoxContainer>
<RecordFieldList
instanceId={`fields-card-${objectRecordId}-${isInRightDrawer ? 'right-drawer' : ''}`}
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
showDuplicatesSection={showDuplicatesSection}
/>
<RecordFieldListCellEditModePortal
objectMetadataItem={objectMetadataItem}
recordId={objectRecordId}
/>
</RecordFieldListComponentInstanceContext.Provider>
</StyledPropertyBoxContainer>
);
};
@@ -1,11 +0,0 @@
import styled from '@emotion/styled';
import { Section } from 'twenty-ui/layout';
const StyledRecordDetailSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
padding: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
width: auto;
`;
export { StyledRecordDetailSection as RecordDetailSection };