Ensure record fields uniqueness across widgets (#16781)

This PR dissociates record inputs so that several inputs for the same
record field can live on the same page.

## Simple values demo


https://github.com/user-attachments/assets/a8c224d8-4cd6-4fe3-9cf8-01f6bdd2fca9

## Relations demo


https://github.com/user-attachments/assets/87267a8b-8ce5-41ce-8a78-cba2384828bb

## Doesn't break Record Table


https://github.com/user-attachments/assets/bcdd2b05-7cb4-4ed2-9093-58be3e04e43e

## Doesn't break Show Page


https://github.com/user-attachments/assets/f9ab9e14-012c-451e-a35f-01a165523f95
This commit is contained in:
Baptiste Devessier
2025-12-24 17:01:10 +01:00
committed by GitHub
parent 6a4974e285
commit 06d0ac13c4
37 changed files with 620 additions and 436 deletions
@@ -14,12 +14,14 @@ import { RecordBoardCardBody } from '@/object-record/record-board/record-board-c
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
import { RecordBoardCardMultiDragStack } from '@/object-record/record-board/record-board-card/components/RecordBoardCardMultiDragStack';
import { RECORD_BOARD_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardClickOutsideId';
import { RECORD_BOARD_CARD_INPUT_ID_PREFIX } from '@/object-record/record-board/record-board-card/constants/RecordBoardCardInputIdPrefix';
import { RecordBoardCardComponentInstanceContext } from '@/object-record/record-board/record-board-card/states/contexts/RecordBoardCardComponentInstanceContext';
import { recordBoardCardIsExpandedComponentState } from '@/object-record/record-board/record-board-card/states/recordBoardCardIsExpandedComponentState';
import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext';
import { RecordCard } from '@/object-record/record-card/components/RecordCard';
import { isRecordIdPrimaryDragMultipleComponentFamilyState } from '@/object-record/record-drag/states/isRecordIdPrimaryDragMultipleComponentFamilyState';
import { isRecordIdSecondaryDragMultipleComponentFamilyState } from '@/object-record/record-drag/states/isRecordIdSecondaryDragMultipleComponentFamilyState';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
@@ -151,34 +153,40 @@ export const RecordBoardCard = () => {
instanceId: `record-board-card-${recordId}`,
}}
>
<StyledBoardCardWrapper
data-click-outside-id={RECORD_BOARD_CARD_CLICK_OUTSIDE_ID}
onContextMenu={handleContextMenuOpen}
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: RECORD_BOARD_CARD_INPUT_ID_PREFIX }}
>
<StyledCardContainer isPrimaryMultiDrag={isRecordIdPrimaryDragMultiple}>
{isRecordIdPrimaryDragMultiple && <RecordBoardCardMultiDragStack />}
<RecordCard
data-selected={isCurrentCardSelected}
data-focused={isCurrentCardFocused}
data-active={isCurrentCardActive}
onMouseLeave={onMouseLeaveBoard}
onClick={handleCardClick}
<StyledBoardCardWrapper
data-click-outside-id={RECORD_BOARD_CARD_CLICK_OUTSIDE_ID}
onContextMenu={handleContextMenuOpen}
>
<StyledCardContainer
isPrimaryMultiDrag={isRecordIdPrimaryDragMultiple}
isSecondaryDragged={isRecordIdSecondaryDragMultiple}
isDragging={isDraggingThisCard}
>
<RecordBoardCardHeader />
<AnimatedEaseInOut
isOpen={isCardExpanded || !isCompactModeActive}
initial={false}
{isRecordIdPrimaryDragMultiple && <RecordBoardCardMultiDragStack />}
<RecordCard
data-selected={isCurrentCardSelected}
data-focused={isCurrentCardFocused}
data-active={isCurrentCardActive}
onMouseLeave={onMouseLeaveBoard}
onClick={handleCardClick}
isPrimaryMultiDrag={isRecordIdPrimaryDragMultiple}
isSecondaryDragged={isRecordIdSecondaryDragMultiple}
isDragging={isDraggingThisCard}
>
<RecordBoardCardBody />
</AnimatedEaseInOut>
</RecordCard>
</StyledCardContainer>
<RecordBoardCardCellHoveredPortal />
<RecordBoardCardCellEditModePortal />
</StyledBoardCardWrapper>
<RecordBoardCardHeader />
<AnimatedEaseInOut
isOpen={isCardExpanded || !isCompactModeActive}
initial={false}
>
<RecordBoardCardBody />
</AnimatedEaseInOut>
</RecordCard>
</StyledCardContainer>
<RecordBoardCardCellHoveredPortal />
<RecordBoardCardCellEditModePortal />
</StyledBoardCardWrapper>
</RecordFieldsScopeContextProvider>
</RecordBoardCardComponentInstanceContext.Provider>
);
};
@@ -6,10 +6,12 @@ import { RecordCalendarCardCellHoveredPortal } from '@/object-record/record-cale
import { RecordCalendarCardBody } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardBody';
import { RecordCalendarCardHeader } from '@/object-record/record-calendar/record-calendar-card/components/RecordCalendarCardHeader';
import { RECORD_CALENDAR_CARD_CLICK_OUTSIDE_ID } from '@/object-record/record-calendar/record-calendar-card/constants/RecordCalendarCardClickOutsideId';
import { RECORD_CALENDAR_CARD_INPUT_ID_PREFIX } from '@/object-record/record-calendar/record-calendar-card/constants/RecordCalendarCardInputIdPrefix';
import { RecordCalendarCardComponentInstanceContext } from '@/object-record/record-calendar/record-calendar-card/states/contexts/RecordCalendarCardComponentInstanceContext';
import { isRecordCalendarCardSelectedComponentFamilyState } from '@/object-record/record-calendar/record-calendar-card/states/isRecordCalendarCardSelectedComponentFamilyState';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { RecordCard } from '@/object-record/record-card/components/RecordCard';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
@@ -78,22 +80,26 @@ export const RecordCalendarCard = ({ recordId }: RecordCalendarCardProps) => {
instanceId: recordId,
}}
>
<StyledContainer onContextMenu={handleContextMenuOpen}>
<StyledRecordCard
data-selected={isCurrentCardSelected}
data-click-outside-id={RECORD_CALENDAR_CARD_CLICK_OUTSIDE_ID}
>
<RecordCalendarCardHeader recordId={recordId} />
<AnimatedEaseInOut isOpen={!isCompactModeActive} initial={false}>
<RecordCalendarCardBody
recordId={recordId}
isRecordReadOnly={false}
/>
</AnimatedEaseInOut>
</StyledRecordCard>
<RecordCalendarCardCellHoveredPortal recordId={recordId} />
<RecordCalendarCardCellEditModePortal recordId={recordId} />
</StyledContainer>
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: RECORD_CALENDAR_CARD_INPUT_ID_PREFIX }}
>
<StyledContainer onContextMenu={handleContextMenuOpen}>
<StyledRecordCard
data-selected={isCurrentCardSelected}
data-click-outside-id={RECORD_CALENDAR_CARD_CLICK_OUTSIDE_ID}
>
<RecordCalendarCardHeader recordId={recordId} />
<AnimatedEaseInOut isOpen={!isCompactModeActive} initial={false}>
<RecordCalendarCardBody
recordId={recordId}
isRecordReadOnly={false}
/>
</AnimatedEaseInOut>
</StyledRecordCard>
<RecordCalendarCardCellHoveredPortal recordId={recordId} />
<RecordCalendarCardCellEditModePortal recordId={recordId} />
</StyledContainer>
</RecordFieldsScopeContextProvider>
</RecordCalendarCardComponentInstanceContext.Provider>
);
};
@@ -0,0 +1,12 @@
import { createRequiredContext } from '~/utils/createRequiredContext';
export type RecordFieldsScopeContextValue = {
scopeInstanceId: string;
};
export const [
RecordFieldsScopeContextProvider,
useRecordFieldsScopeContextOrThrow,
] = createRequiredContext<RecordFieldsScopeContextValue>(
'RecordFieldsScopeContext',
);
@@ -1,6 +1,7 @@
import { useContext } from 'react';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { RecordDetailSectionContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailSectionContainer';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import {
@@ -26,6 +27,7 @@ type RecordDetailMorphRelationSectionProps = {
export const RecordDetailMorphRelationSection = ({
loading,
}: RecordDetailMorphRelationSectionProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { recordId, fieldDefinition } = useContext(FieldContext);
const metadata = fieldDefinition.metadata as FieldMorphRelationMetadata;
@@ -44,6 +46,7 @@ export const RecordDetailMorphRelationSection = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const isDropdownOpen = useRecoilComponentValue(
@@ -1,6 +1,7 @@
import { type ReactNode, useCallback, useContext } from 'react';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useGetMorphRelationRelatedRecordsWithObjectNameSingular } from '@/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
@@ -29,6 +30,7 @@ type RecordDetailMorphRelationSectionDropdownManyToOneProps = {
export const RecordDetailMorphRelationSectionDropdownManyToOne = ({
dropdownTriggerClickableComponent,
}: RecordDetailMorphRelationSectionDropdownManyToOneProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { recordId, fieldDefinition } = useContext(FieldContext);
assertFieldMetadata(
@@ -58,6 +60,7 @@ export const RecordDetailMorphRelationSectionDropdownManyToOne = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const { closeDropdown } = useCloseDropdown();
@@ -1,6 +1,7 @@
import { type ReactNode, useCallback, useContext } from 'react';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useGetMorphRelationRelatedRecordsWithObjectNameSingular } from '@/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useUpdateMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput';
@@ -31,6 +32,7 @@ type RecordDetailMorphRelationSectionDropdownOneToManyProps = {
export const RecordDetailMorphRelationSectionDropdownOneToMany = ({
dropdownTriggerClickableComponent,
}: RecordDetailMorphRelationSectionDropdownOneToManyProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { recordId, fieldDefinition } = useContext(FieldContext);
assertFieldMetadata(
@@ -61,6 +63,7 @@ export const RecordDetailMorphRelationSectionDropdownOneToMany = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const { closeDropdown } = useCloseDropdown();
@@ -12,6 +12,10 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList';
import {
RecordFieldsScopeContextProvider,
useRecordFieldsScopeContextOrThrow,
} from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
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';
@@ -30,13 +34,13 @@ 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 { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { createPortal } from 'react-dom';
import {
computeMorphRelationFieldName,
CustomError,
} from 'twenty-shared/utils';
import { Trans } from '@lingui/react/macro';
import { t } from '@lingui/core/macro';
import {
IconChevronDown,
IconDotsVertical,
@@ -99,6 +103,7 @@ export const RecordDetailRelationRecordsListItem = ({
relationObjectMetadataNameSingular,
relationFieldMetadataId,
}: RecordDetailRelationRecordsListItemProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const {
fieldDefinition,
recordId,
@@ -162,6 +167,7 @@ export const RecordDetailRelationRecordsListItem = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const setSingleRecordPickerSelectedId = useSetRecoilComponentState(
singleRecordPickerSelectedIdComponentState,
@@ -284,15 +290,21 @@ export const RecordDetailRelationRecordsListItem = ({
)}
</StyledListItem>
<AnimatedEaseInOut isOpen={isExpanded}>
<RecordFieldList
instanceId={`record-detail-relation-${relationRecord.id}-${isInRightDrawer ? 'right-drawer' : ''}`}
objectNameSingular={relationObjectMetadataNameSingular}
objectRecordId={relationRecord.id}
showDuplicatesSection={false}
showRelationSections={false}
excludeCreatedAtAndUpdatedAt={true}
excludeFieldMetadataIds={[relationFieldMetadataId]}
/>
<RecordFieldsScopeContextProvider
value={{
scopeInstanceId: `record-detail-relation-${relationRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`,
}}
>
<RecordFieldList
instanceId={`record-detail-relation-${relationRecord.id}-${isInRightDrawer ? 'right-drawer' : ''}`}
objectNameSingular={relationObjectMetadataNameSingular}
objectRecordId={relationRecord.id}
showDuplicatesSection={false}
showRelationSections={false}
excludeCreatedAtAndUpdatedAt={true}
excludeFieldMetadataIds={[relationFieldMetadataId]}
/>
</RecordFieldsScopeContextProvider>
</AnimatedEaseInOut>
{createPortal(
<ConfirmationModal
@@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
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';
@@ -46,6 +47,7 @@ export const RecordDetailRelationSection = ({
}: RecordDetailRelationSectionProps) => {
const { t } = useLingui();
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { recordId, fieldDefinition } = useContext(FieldContext);
const {
@@ -95,6 +97,7 @@ export const RecordDetailRelationSection = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const isDropdownOpen = useRecoilComponentValue(
@@ -4,6 +4,7 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput';
@@ -33,6 +34,7 @@ type RecordDetailRelationSectionDropdownToManyProps = {
export const RecordDetailRelationSectionDropdownToMany = ({
dropdownTriggerClickableComponent,
}: RecordDetailRelationSectionDropdownToManyProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { recordId, fieldDefinition } = useContext(FieldContext);
const { fieldMetadataId } = fieldDefinition;
const {
@@ -78,6 +80,7 @@ export const RecordDetailRelationSectionDropdownToMany = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const { closeDropdown } = useCloseDropdown();
@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
@@ -36,6 +37,7 @@ type RecordDetailRelationSectionDropdownToOneProps = {
export const RecordDetailRelationSectionDropdownToOne = ({
dropdownTriggerClickableComponent,
}: RecordDetailRelationSectionDropdownToOneProps) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { recordId, fieldDefinition } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.RELATION,
@@ -89,6 +91,7 @@ export const RecordDetailRelationSectionDropdownToOne = ({
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
fieldDefinition,
recordId,
instanceId: scopeInstanceId,
});
const { closeDropdown } = useCloseDropdown();
@@ -10,6 +10,7 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { getCompaniesMock } from '~/testing/mock-data/companies';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RightDrawerDecorator } from '~/testing/decorators/RightDrawerDecorator';
@@ -49,7 +50,11 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
isRecordFieldReadOnly: false,
}}
>
<Story />
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: 'mock-instance-id' }}
>
<Story />
</RecordFieldsScopeContextProvider>
</FieldContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
),
@@ -5,6 +5,7 @@ import { useEffect } from 'react';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/ui/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
@@ -90,11 +91,15 @@ const DateFieldInputWithContext = ({
isRecordFieldReadOnly: false,
}}
>
<FieldInputEventContextProviderWithJestMocks>
<StorybookFieldInputDropdownFocusIdSetterEffect />
<DateFieldValueSetterEffect value={value} />
<DateFieldValueGater />
</FieldInputEventContextProviderWithJestMocks>
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: instanceId }}
>
<FieldInputEventContextProviderWithJestMocks>
<StorybookFieldInputDropdownFocusIdSetterEffect />
<DateFieldValueSetterEffect value={value} />
<DateFieldValueGater />
</FieldInputEventContextProviderWithJestMocks>
</RecordFieldsScopeContextProvider>
</FieldContext.Provider>
<div data-testid="data-field-input-click-outside-div"></div>
</RecordFieldComponentInstanceContext.Provider>
@@ -6,6 +6,7 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { getFieldInputEventContextProviderWithJestMocks } from '@/object-record/record-field/ui/meta-types/input/components/__stories__/utils/getFieldInputEventContextProviderWithJestMocks';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
@@ -96,11 +97,15 @@ const NumberFieldInputWithContext = ({
isRecordFieldReadOnly: false,
}}
>
<FieldInputEventContextProviderWithJestMocks>
{isReady && <StorybookFieldInputDropdownFocusIdSetterEffect />}
<NumberFieldValueSetterEffect value={value} />
<NumberFieldInput />
</FieldInputEventContextProviderWithJestMocks>
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: RECORD_TABLE_CELL_INPUT_ID_PREFIX }}
>
<FieldInputEventContextProviderWithJestMocks>
{isReady && <StorybookFieldInputDropdownFocusIdSetterEffect />}
<NumberFieldValueSetterEffect value={value} />
<NumberFieldInput />
</FieldInputEventContextProviderWithJestMocks>
</RecordFieldsScopeContextProvider>
</FieldContext.Provider>
{isReady && <div data-testid="is-ready-marker" />}
<div data-testid="data-field-input-click-outside-div" />
@@ -9,6 +9,7 @@ import { useGetButtonIcon } from '@/object-record/record-field/ui/hooks/useGetBu
import { useIsFieldInputOnly } from '@/object-record/record-field/ui/hooks/useIsFieldInputOnly';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/ui/hooks/useOpenFieldInputEditMode';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import {
FieldInputEventContext,
type FieldInputClickOutsideEvent,
@@ -44,6 +45,7 @@ export const RecordInlineCell = ({
onCloseEditMode: onCloseEditModeFromContext,
isRecordFieldReadOnly: isReadOnly,
} = useContext(FieldContext);
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
@@ -142,11 +144,12 @@ export const RecordInlineCell = ({
.getLoadable(activeDropdownFocusIdState)
.getValue();
const expectedDropdownFocusId = getDropdownFocusIdForRecordField(
const expectedDropdownFocusId = getDropdownFocusIdForRecordField({
recordId,
fieldDefinition.fieldMetadataId,
'inline-cell',
);
fieldMetadataId: fieldDefinition.fieldMetadataId,
componentType: 'inline-cell',
instanceId: scopeInstanceId,
});
if (currentDropdownFocusId !== expectedDropdownFocusId) {
return;
@@ -162,9 +165,10 @@ export const RecordInlineCell = ({
closeInlineCell();
},
[
closeInlineCell,
recordId,
fieldDefinition.fieldMetadataId,
scopeInstanceId,
closeInlineCell,
persistFieldFromFieldInputContext,
],
);
@@ -101,8 +101,9 @@ export const RecordInlineCellAnchoredPortal = ({
>
<RecordInlineCellAnchoredPortalContext>
{children}
<RecordInlineCellCloseOnCommandMenuOpeningEffect />
</RecordInlineCellAnchoredPortalContext>
<RecordInlineCellCloseOnCommandMenuOpeningEffect />
</RecordFieldComponentInstanceContext.Provider>,
anchorElement,
)}
@@ -2,6 +2,7 @@ import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useInitDraftValue } from '@/object-record/record-field/ui/hooks/useInitDraftValue';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
@@ -21,6 +22,7 @@ export const useInlineCell = (
);
const { onOpenEditMode, onCloseEditMode } = useRecordInlineCellContext();
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
@@ -43,13 +45,14 @@ export const useInlineCell = (
fieldComponentInstanceId: recordFieldComponentInstanceId,
});
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'inline-cell',
),
);
const dropdownId = getDropdownFocusIdForRecordField({
recordId,
fieldMetadataId: fieldDefinition.fieldMetadataId,
componentType: 'inline-cell',
instanceId: scopeInstanceId,
});
setActiveDropdownFocusIdAndMemorizePrevious(dropdownId);
};
return {
@@ -1,4 +1,5 @@
import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { type FieldCardConfiguration } from '@/object-record/record-show/types/CardConfiguration';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
@@ -11,12 +12,16 @@ export const FieldsCard = ({ configuration }: FieldsCardProps) => {
const targetRecord = useTargetRecord();
const { isInRightDrawer } = useLayoutRenderingContext();
const instanceId = `fields-card-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`;
return (
<RecordFieldList
instanceId={`fields-card-${targetRecord.id}-${isInRightDrawer ? 'right-drawer' : ''}`}
objectNameSingular={targetRecord.targetObjectNameSingular}
objectRecordId={targetRecord.id}
showDuplicatesSection={configuration?.showDuplicatesSection ?? true}
/>
<RecordFieldsScopeContextProvider value={{ scopeInstanceId: instanceId }}>
<RecordFieldList
instanceId={instanceId}
objectNameSingular={targetRecord.targetObjectNameSingular}
objectRecordId={targetRecord.id}
showDuplicatesSection={configuration?.showDuplicatesSection ?? true}
/>
</RecordFieldsScopeContextProvider>
);
};
@@ -4,9 +4,11 @@ import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldM
type GetRecordFieldCardRelationPickerDropdownIdArgs = {
fieldDefinition: FieldDefinition<FieldMetadata>;
recordId: string;
instanceId: string;
};
export const getRecordFieldCardRelationPickerDropdownId = ({
fieldDefinition,
recordId,
instanceId,
}: GetRecordFieldCardRelationPickerDropdownIdArgs) =>
`record-field-card-relation-picker-${fieldDefinition.fieldMetadataId}-${recordId}`;
`record-field-card-relation-picker:${instanceId}:${fieldDefinition.fieldMetadataId}:${recordId}`;
@@ -4,8 +4,10 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { RECORD_TABLE_COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnMinWidth';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
@@ -46,22 +48,26 @@ export const RecordTableContextProvider = ({
: 'MOUSE_DOWN';
return (
<RecordTableContextInternalProvider
value={{
viewBarId,
objectMetadataItem,
recordTableId,
objectNameSingular,
objectPermissions,
visibleRecordFields: visibleRecordFields.map((field) => ({
...field,
size: Math.max(field.size, RECORD_TABLE_COLUMN_MIN_WIDTH),
})),
onRecordIdentifierClick,
triggerEvent,
}}
<RecordFieldsScopeContextProvider
value={{ scopeInstanceId: RECORD_TABLE_CELL_INPUT_ID_PREFIX }}
>
{children}
</RecordTableContextInternalProvider>
<RecordTableContextInternalProvider
value={{
viewBarId,
objectMetadataItem,
recordTableId,
objectNameSingular,
objectPermissions,
visibleRecordFields: visibleRecordFields.map((field) => ({
...field,
size: Math.max(field.size, RECORD_TABLE_COLUMN_MIN_WIDTH),
})),
onRecordIdentifierClick,
triggerEvent,
}}
>
{children}
</RecordTableContextInternalProvider>
</RecordFieldsScopeContextProvider>
);
};
@@ -21,6 +21,7 @@ import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropd
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
@@ -41,6 +42,8 @@ export type OpenTableCellArgs = {
};
export const useOpenRecordTableCell = (recordTableId: string) => {
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
const clickOutsideListenerIsActivatedState = useRecoilComponentCallbackState(
clickOutsideListenerIsActivatedComponentState,
RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
@@ -157,11 +160,12 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
toggleClickOutside(false);
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
getDropdownFocusIdForRecordField({
recordId,
fieldDefinition.fieldMetadataId,
'table-cell',
),
fieldMetadataId: fieldDefinition.fieldMetadataId,
componentType: 'table-cell',
instanceId: scopeInstanceId,
}),
);
},
[
@@ -175,10 +179,11 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
initDraftValue,
toggleClickOutside,
setActiveDropdownFocusIdAndMemorizePrevious,
scopeInstanceId,
leaveTableFocus,
openRecordFromIndexView,
activateRecordTableRow,
unfocusRecordTableRow,
openRecordFromIndexView,
],
);
@@ -1,7 +1,13 @@
export const getDropdownFocusIdForRecordField = (
recordId: string,
fieldMetadataId: string,
componentType: 'table-cell' | 'inline-cell',
) => {
return `dropdown-${componentType}-record-${recordId}-field-${fieldMetadataId}`;
export const getDropdownFocusIdForRecordField = ({
recordId,
fieldMetadataId,
componentType,
instanceId,
}: {
recordId: string;
fieldMetadataId: string;
componentType: 'table-cell' | 'inline-cell';
instanceId: string;
}) => {
return `dropdown-${instanceId}-${componentType}-record-${recordId}-field-${fieldMetadataId}`;
};