fix: prevent duplicate dropdowns in activity targets when editing in different contexts (#12462)

Closes #12361

### Changes Made

- In `ActivityTargetsInlineCell`, we now use different component
instance IDs based on context. This ensures that each instance of the
component (whether in right drawer or main view) has its own isolated
state, preventing state conflicts and duplicate dropdowns.
- The `MultipleRecordPicker` component now properly resets its state
when closed, preventing state leakage between instances.


https://github.com/user-attachments/assets/deb99687-a803-417e-a339-cab061026739
This commit is contained in:
Abdul Rahman
2025-06-09 18:50:13 +05:30
committed by GitHub
parent da35a2f479
commit 1f2c40af61
7 changed files with 69 additions and 6 deletions

View File

@ -0,0 +1,11 @@
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
export const useActivityTargetsComponentInstanceId = (
baseComponentInstanceId: string,
) => {
const { isInRightDrawer } = useIsInRightDrawerOrThrow();
return isInRightDrawer
? `${baseComponentInstanceId}-right-drawer`
: baseComponentInstanceId;
};

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useActivityTargetsComponentInstanceId } from '@/activities/inline-cell/hooks/useActivityTargetsComponentInstanceId';
import { Note } from '@/activities/types/Note';
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
@ -72,6 +73,11 @@ export const NoteCard = ({
const body = getActivityPreview(note?.bodyV2?.blocknote ?? null);
const baseComponentInstanceId = `note-card-${note.id}-targets`;
const componentInstanceId = useActivityTargetsComponentInstanceId(
baseComponentInstanceId,
);
return (
<StyledCard isSingleNote={isSingleNote}>
<StyledCardDetailsContainer
@ -93,7 +99,7 @@ export const NoteCard = ({
fieldPosition={0}
>
<ActivityTargetsInlineCell
componentInstanceId={`note-card-${note.id}-targets`}
componentInstanceId={componentInstanceId}
activityRecordId={note.id}
activityObjectNameSingular={CoreObjectNameSingular.Note}
/>

View File

@ -6,6 +6,7 @@ import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
import { ActivityRow } from '@/activities/components/ActivityRow';
import { useActivityTargetsComponentInstanceId } from '@/activities/inline-cell/hooks/useActivityTargetsComponentInstanceId';
import { Task } from '@/activities/types/Task';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -81,6 +82,11 @@ export const TaskRow = ({ task }: { task: Task }) => {
const { completeTask } = useCompleteTask(task);
const baseComponentInstanceId = `task-row-targets-${task.id}`;
const componentInstanceId = useActivityTargetsComponentInstanceId(
baseComponentInstanceId,
);
return (
<ActivityRow
onClick={() => {
@ -131,7 +137,7 @@ export const TaskRow = ({ task }: { task: Task }) => {
activityRecordId={task.id}
showLabel={false}
maxWidth={200}
componentInstanceId={`task-row-targets-${task.id}`}
componentInstanceId={componentInstanceId}
/>
</StopPropagationContainer>
</FieldContextProvider>

View File

@ -2,6 +2,7 @@ import { MultipleRecordPickerItemsDisplay } from '@/object-record/record-picker/
import { MultipleRecordPickerOnClickOutsideEffect } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerOnClickOutsideEffect';
import { MultipleRecordPickerSearchInput } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPickerSearchInput';
import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext';
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
import { getMultipleRecordPickerSelectableListId } from '@/object-record/record-picker/multiple-record-picker/utils/getMultipleRecordPickerSelectableListId';
@ -60,12 +61,37 @@ export const MultipleRecordPicker = ({
componentInstanceId,
);
const multipleRecordPickerPickableMorphItemsState =
useRecoilComponentCallbackStateV2(
multipleRecordPickerPickableMorphItemsComponentState,
componentInstanceId,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const resetState = useRecoilCallback(
({ set }) => {
return () => {
set(multipleRecordPickerPickableMorphItemsState, []);
set(multipleRecordPickerSearchFilterState, '');
};
},
[
multipleRecordPickerPickableMorphItemsState,
multipleRecordPickerSearchFilterState,
],
);
const handleSubmit = () => {
onSubmit?.();
goBackToPreviousHotkeyScope();
resetSelectedItem();
resetState();
};
const handleClickOutside = () => {
onClickOutside();
resetState();
};
useScopedHotkeys(
@ -108,7 +134,7 @@ export const MultipleRecordPicker = ({
>
<MultipleRecordPickerOnClickOutsideEffect
containerRef={containerRef}
onClickOutside={onClickOutside}
onClickOutside={handleClickOutside}
/>
<DropdownContent ref={containerRef}>
{layoutDirection === 'search-bar-on-bottom' && (

View File

@ -20,6 +20,7 @@ import { RecordDetailDuplicatesSection } from '@/object-record/record-show/recor
import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { FieldMetadataType } from '~/generated/graphql';
type FieldsCardProps = {
@ -48,6 +49,8 @@ export const FieldsCard = ({
objectRecordId,
});
const { isInRightDrawer } = useIsInRightDrawerOrThrow();
const availableFieldMetadataItems = objectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
@ -139,7 +142,9 @@ export const FieldsCard = ({
componentInstanceId={getRecordFieldInputId(
objectRecordId,
fieldMetadataItem.name,
'fields-card',
isInRightDrawer
? 'right-drawer-fields-card'
: 'fields-card',
)}
activityObjectNameSingular={
objectNameSingular as

View File

@ -1,5 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
@ -49,7 +50,7 @@ export const RecordShowContainer = ({
);
return (
<>
<RightDrawerProvider value={{ isInRightDrawer }}>
<RecordShowContainerContextStoreTargetedRecordsEffect
recordId={objectRecordId}
/>
@ -71,6 +72,6 @@ export const RecordShowContainer = ({
loading={isPrefetchLoading || loading || recordLoading}
/>
</ShowPageContainer>
</>
</RightDrawerProvider>
);
};

View File

@ -0,0 +1,8 @@
import { createRequiredContext } from '~/utils/createRequiredContext';
type RightDrawerContextType = {
isInRightDrawer: boolean;
};
export const [RightDrawerProvider, useIsInRightDrawerOrThrow] =
createRequiredContext<RightDrawerContextType>('RightDrawer');