From cec23e89facbc26e1a502162a6b86482262b6b8a Mon Sep 17 00:00:00 2001 From: Samuel Arbibe Date: Tue, 24 Mar 2026 18:14:30 +0200 Subject: [PATCH] Fix batch update optimistic and prevent accidental mass-update (#17213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem - ⚠️ Multi-edit could silently update ALL records of an object with no undo, no ctrl-z - After a batch update the table showed stale data — `useIncrementalUpdateManyRecords` had no explicit `findMany` refetch and `skipOptimisticEffect` was missing - Clicking inside the side panel deselected all kanban cards — `RecordBoardClickOutsideEffect` uses `refs:[]` and the side panel had no `data-click-outside-id` - Clicking a currency/select dropdown inside the panel also triggered deselection — `FloatingPortal` renders outside the side panel DOM, bypassing the click-outside-id exclusion - An empty selection silently matched every record — `computeContextStoreFilters` returned `undefined` filter when `selectedRecordIds` was `[]` ## Fix - Table refreshes correctly after batch update — `useRefetchFindManyRecords` explicitly refetches `FindMany` queries; `useIncrementalUpdateManyRecords` adds `skipOptimisticEffect: true` and calls it in `finally` - Clicking the side panel no longer deselects kanban cards — `SidePanelForDesktop` carries `data-click-outside-id`; `RecordBoardClickOutsideEffect` + `RecordTableBodyFocusClickOutsideEffect` exclude it - Clicking dropdowns inside the panel no longer deselects either — `ParentClickOutsideIdContext` propagates the side panel ID into `FloatingPortal` content via `DropdownInternalContainer` - Empty selection can no longer match all records — `computeContextStoreFilters` returns `{ id: { in: [] } }` instead of `undefined` - Apply is disabled with no selection; a confirmation modal shows the count + no-undo warning before executing — `UpdateMultipleRecordsContainer` ## Not included - Undo / snapshot restore — requires backend changes, out of scope ## Blast radius - `ParentClickOutsideIdContext` touches `DropdownInternalContainer` (207 `` usages). `parentClickOutsideId` is `undefined` everywhere outside the side panel → attribute not rendered → zero behavioral change for existing consumers. --------- Co-authored-by: Samuel Arbibe Co-authored-by: Lucas Bordeau Co-authored-by: Félix Malfait --- .../utils/computeContextStoreFilters.ts | 16 ++++--- .../useIncrementalUpdateManyRecords.test.tsx | 15 +++++++ .../hooks/useIncrementalUpdateManyRecords.ts | 9 +++- .../hooks/useRefetchFindManyRecords.ts | 24 ++++++++++ .../RecordBoardClickOutsideEffect.tsx | 2 + ...RecordTableBodyFocusClickOutsideEffect.tsx | 2 + .../UpdateMultipleRecordsContainer.tsx | 44 ++++++++++++++----- ...UpdateMultipleRecordsContainer.stories.tsx | 23 +++++++++- .../hooks/useUpdateMultipleRecordsActions.ts | 7 +-- .../components/SidePanelForDesktop.tsx | 9 +++- .../constants/SidePanelClickOutsideId.ts | 1 + .../internal/DropdownInternalContainer.tsx | 8 +++- .../contexts/ParentClickOutsideIdContext.tsx | 5 +++ 13 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useRefetchFindManyRecords.ts create mode 100644 packages/twenty-front/src/modules/side-panel/constants/SidePanelClickOutsideId.ts create mode 100644 packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext.tsx diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index 24e3e34b17..dfb621a476 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -58,15 +58,17 @@ export const computeContextStoreFilters = ({ ]); } if (contextStoreTargetedRecordsRule.mode === 'selection') { + if (contextStoreTargetedRecordsRule.selectedRecordIds.length === 0) { + return { id: { in: [] } }; + } + queryFilter = makeAndFilterVariables([ recordGqlFilterForAnyFieldFilter, - contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 - ? { - id: { - in: contextStoreTargetedRecordsRule.selectedRecordIds, - }, - } - : undefined, + { + id: { + in: contextStoreTargetedRecordsRule.selectedRecordIds, + }, + }, computeRecordGqlOperationFilter({ filterValueDependencies, fields: objectMetadataItem?.fields ?? [], diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useIncrementalUpdateManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useIncrementalUpdateManyRecords.test.tsx index 6d37780d7c..c19a2840ab 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useIncrementalUpdateManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useIncrementalUpdateManyRecords.test.tsx @@ -1,6 +1,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords'; import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords'; +import { useRefetchFindManyRecords } from '@/object-record/hooks/useRefetchFindManyRecords'; import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords'; import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent'; import { renderHook } from '@testing-library/react'; @@ -15,6 +16,9 @@ jest.mock('@/object-record/hooks/useRefetchAggregateQueries', () => ({ refetchAggregateQueries: jest.fn(), }), })); +jest.mock('@/object-record/hooks/useRefetchFindManyRecords', () => ({ + useRefetchFindManyRecords: jest.fn(), +})); jest.mock('@/object-record/hooks/useIncrementalFetchAndMutateRecords'); const mockUseObjectMetadataItem = jest.mocked(useObjectMetadataItem); @@ -25,6 +29,7 @@ const mockUseUpdateManyRecords = jest.mocked(useUpdateManyRecords); const mockUseIncrementalFetchAndMutateRecords = jest.mocked( useIncrementalFetchAndMutateRecords, ); +const mockUseRefetchFindManyRecords = jest.mocked(useRefetchFindManyRecords); describe('useIncrementalUpdateManyRecords', () => { const mockUpdateManyRecords = jest.fn(); @@ -53,6 +58,11 @@ describe('useIncrementalUpdateManyRecords', () => { updateProgress: mockUpdateProgress, cancel: jest.fn(), }); + + const mockRefetchFindManyRecords = jest.fn(); + mockUseRefetchFindManyRecords.mockReturnValue({ + refetchFindManyRecords: mockRefetchFindManyRecords, + }); }); it('should call incrementalFetchAndMutate and execute mutations via useUpdateManyRecords', async () => { @@ -81,6 +91,7 @@ describe('useIncrementalUpdateManyRecords', () => { delayInMsBetweenRequests: 50, skipRegisterObjectOperation: true, skipRefetchAggregateQueries: true, + skipOptimisticEffect: true, abortSignal: expect.any(AbortSignal), }); expect(mockUpdateProgress).toHaveBeenCalledWith(2, 2); @@ -102,6 +113,10 @@ describe('useIncrementalUpdateManyRecords', () => { }, }, }); + + const { refetchFindManyRecords } = + mockUseRefetchFindManyRecords.mock.results[0].value; + expect(refetchFindManyRecords).toHaveBeenCalled(); }); it('should pass abortSignal to updateManyRecords', async () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/useIncrementalUpdateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useIncrementalUpdateManyRecords.ts index 32b92404b9..d4e66022c3 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useIncrementalUpdateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useIncrementalUpdateManyRecords.ts @@ -1,11 +1,12 @@ +import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize'; import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination'; import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords'; import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries'; +import { useRefetchFindManyRecords } from '@/object-record/hooks/useRefetchFindManyRecords'; import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent'; import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput'; const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50; @@ -39,6 +40,10 @@ export const useIncrementalUpdateManyRecords = < const { refetchAggregateQueries } = useRefetchAggregateQueries(); + const { refetchFindManyRecords } = useRefetchFindManyRecords({ + objectMetadataNamePlural: objectMetadataItem.namePlural, + }); + const { incrementalFetchAndMutate, progress, @@ -68,6 +73,7 @@ export const useIncrementalUpdateManyRecords = < delayInMsBetweenRequests: delayInMsBetweenMutations, skipRegisterObjectOperation: true, skipRefetchAggregateQueries: true, + skipOptimisticEffect: true, abortSignal, }); @@ -78,6 +84,7 @@ export const useIncrementalUpdateManyRecords = < }, ); } finally { + await refetchFindManyRecords(); await refetchAggregateQueries({ objectMetadataNamePlural: objectMetadataItem.namePlural, }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRefetchFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useRefetchFindManyRecords.ts new file mode 100644 index 0000000000..73263accf8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useRefetchFindManyRecords.ts @@ -0,0 +1,24 @@ +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { capitalize } from 'twenty-shared/utils'; + +export const useRefetchFindManyRecords = ({ + objectMetadataNamePlural, +}: { + objectMetadataNamePlural: string; +}) => { + const apolloCoreClient = useApolloCoreClient(); + + const refetchFindManyRecords = async () => { + const findManyRecordsQueryName = `FindMany${capitalize( + objectMetadataNamePlural, + )}`; + + await apolloCoreClient.refetchQueries({ + include: [findManyRecordsQueryName], + }); + }; + + return { + refetchFindManyRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx index fe39ef452c..fdedc6ef34 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardClickOutsideEffect.tsx @@ -1,6 +1,7 @@ import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId'; import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId'; import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId'; +import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard'; import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard'; @@ -37,6 +38,7 @@ export const RecordBoardClickOutsideEffect = () => { PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID, RECORD_BOARD_CARD_CLICK_OUTSIDE_ID, LINK_CHIP_CLICK_OUTSIDE_ID, + SIDE_PANEL_CLICK_OUTSIDE_ID, ], listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID, refs: [], diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect.tsx index bd4e0c964f..f287a1c76b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyFocusClickOutsideEffect.tsx @@ -1,6 +1,7 @@ import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId'; import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; +import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus'; import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId'; @@ -30,6 +31,7 @@ export const RecordTableBodyFocusClickOutsideEffect = ({ COMMAND_MENU_CLICK_OUTSIDE_ID, PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID, MODAL_BACKDROP_CLICK_OUTSIDE_ID, + SIDE_PANEL_CLICK_OUTSIDE_ID, ], listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID, refs: [tableBodyRef], diff --git a/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsContainer.tsx index 1ef9fd39ce..37ed6391db 100644 --- a/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsContainer.tsx @@ -1,15 +1,22 @@ +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; import { UpdateMultipleRecordsFooter } from '@/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter'; import { UpdateMultipleRecordsForm } from '@/object-record/record-update-multiple/components/UpdateMultipleRecordsForm'; import { useUpdateMultipleRecordsActions } from '@/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer'; import { SidePanelProvider } from '@/ui/layout/side-panel/contexts/SidePanelContext'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +const UPDATE_MULTIPLE_RECORDS_CONFIRMATION_MODAL_ID = + 'update-multiple-records-confirmation'; + const StyledShowPageRightContainer = styled.div` display: flex; flex-direction: column; @@ -41,23 +48,30 @@ export const UpdateMultipleRecordsContainer = ({ contextStoreInstanceId, }); + const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue( + contextStoreNumberOfSelectedRecordsComponentState, + contextStoreInstanceId, + ); + + const hasSelectedRecords = contextStoreNumberOfSelectedRecords > 0; + const { t } = useLingui(); - const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + const { enqueueErrorSnackBar } = useSnackBar(); + const { openModal } = useModal(); const { closeSidePanelMenu } = useSidePanelMenu(); const [fieldUpdates, setFieldUpdates] = useState( {}, ); - const handleUpdate = async () => { + const handleUpdateClick = () => { + openModal(UPDATE_MULTIPLE_RECORDS_CONFIRMATION_MODAL_ID); + }; + + const handleConfirmedUpdate = async () => { try { - const count = await updateRecords(fieldUpdates); - if (count !== undefined) { - enqueueSuccessSnackBar({ - message: t`Successfully updated ${count} records`, - }); - closeSidePanelMenu(); - } + await updateRecords(fieldUpdates); + closeSidePanelMenu(); } catch (error) { enqueueErrorSnackBar({ message: @@ -99,12 +113,20 @@ export const UpdateMultipleRecordsContainer = ({ + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-update-multiple/components/__stories__/UpdateMultipleRecordsContainer.stories.tsx b/packages/twenty-front/src/modules/object-record/record-update-multiple/components/__stories__/UpdateMultipleRecordsContainer.stories.tsx index 235c06bd32..6376577016 100644 --- a/packages/twenty-front/src/modules/object-record/record-update-multiple/components/__stories__/UpdateMultipleRecordsContainer.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-update-multiple/components/__stories__/UpdateMultipleRecordsContainer.stories.tsx @@ -1,10 +1,17 @@ import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext'; import { UpdateMultipleRecordsContainer } from '@/object-record/record-update-multiple/components/UpdateMultipleRecordsContainer'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { useEffect } from 'react'; import { ApolloClient, InMemoryCache } from '@apollo/client'; import { MockLink } from '@apollo/client/testing'; -import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { + type Decorator, + type Meta, + type StoryObj, +} from '@storybook/react-vite'; import gql from 'graphql-tag'; import { expect, userEvent, within } from 'storybook/test'; import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator'; @@ -46,6 +53,19 @@ const mockApolloCoreClient = new ApolloClient({ cache: new InMemoryCache(), }); +const SelectedRecordsSeedDecorator: Decorator = (Story) => { + const setNumberOfSelectedRecords = useSetAtomComponentState( + contextStoreNumberOfSelectedRecordsComponentState, + MAIN_CONTEXT_STORE_INSTANCE_ID, + ); + + useEffect(() => { + setNumberOfSelectedRecords(3); + }, [setNumberOfSelectedRecords]); + + return ; +}; + const meta: Meta = { title: 'Modules/ObjectRecord/RecordUpdateMultiple/Components/UpdateMultipleRecordsContainer', @@ -65,6 +85,7 @@ const meta: Meta = { ), + SelectedRecordsSeedDecorator, ContextStoreDecorator, ObjectMetadataItemsDecorator, SnackBarDecorator, diff --git a/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts b/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts index 4a824e7fff..1098567c46 100644 --- a/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions.ts @@ -62,13 +62,8 @@ export const useUpdateMultipleRecordsActions = ({ filter: graphqlFilter, }); - const updateRecords = async (fieldsToUpdate: Record) => { - const count = await incrementalUpdateManyRecords(fieldsToUpdate); - return count; - }; - return { - updateRecords, + updateRecords: incrementalUpdateManyRecords, isUpdating, progress, cancel, diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx index 9573ecce14..004f092886 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx @@ -1,5 +1,6 @@ import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter'; import { SidePanelWidthEffect } from '@/side-panel/components/SidePanelWidthEffect'; +import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; import { useSidePanelCloseAnimationCompleteCleanup } from '@/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup'; import { @@ -10,6 +11,7 @@ import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingS import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState'; import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState'; import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext'; +import { ParentClickOutsideIdContext } from '@/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext'; import { ResizablePanelGap } from '@/ui/layout/resizable-panel/components/ResizablePanelGap'; import { SIDE_PANEL_CONSTRAINTS } from '@/side-panel/constants/SidePanelConstraints'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; @@ -138,11 +140,16 @@ export const SidePanelForDesktop = () => { isResizing={isResizing} onTransitionEnd={handleTransitionEnd} data-side-panel="" + data-click-outside-id={SIDE_PANEL_CLICK_OUTSIDE_ID} > - {shouldShowContent && } + + {shouldShowContent && } + diff --git a/packages/twenty-front/src/modules/side-panel/constants/SidePanelClickOutsideId.ts b/packages/twenty-front/src/modules/side-panel/constants/SidePanelClickOutsideId.ts new file mode 100644 index 0000000000..dce325843f --- /dev/null +++ b/packages/twenty-front/src/modules/side-panel/constants/SidePanelClickOutsideId.ts @@ -0,0 +1 @@ +export const SIDE_PANEL_CLICK_OUTSIDE_ID = 'side-panel'; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/internal/DropdownInternalContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/internal/DropdownInternalContainer.tsx index f5103c2c21..15957b01ad 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/internal/DropdownInternalContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/internal/DropdownInternalContainer.tsx @@ -10,6 +10,7 @@ import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContaine import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext'; +import { ParentClickOutsideIdContext } from '@/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; @@ -141,6 +142,7 @@ export const DropdownInternalContainer = ({ }; const { excludedClickOutsideId } = useContext(ClickOutsideListenerContext); + const parentClickOutsideId = useContext(ParentClickOutsideIdContext); return ( <> @@ -162,7 +164,11 @@ export const DropdownInternalContainer = ({ isDropdownInModal={isDropdownInModal} > - + {dropdownComponents} diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext.tsx new file mode 100644 index 0000000000..d83825cc69 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext.tsx @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +export const ParentClickOutsideIdContext = createContext( + undefined, +);