diff --git a/front/src/modules/ui/board/components/EntityBoard.tsx b/front/src/modules/ui/board/components/EntityBoard.tsx index 73764bad1..951598565 100644 --- a/front/src/modules/ui/board/components/EntityBoard.tsx +++ b/front/src/modules/ui/board/components/EntityBoard.tsx @@ -7,12 +7,15 @@ import { useRecoilState } from 'recoil'; import { CompanyBoardRecoilScopeContext } from '@/companies/states/recoil-scope-contexts/CompanyBoardRecoilScopeContext'; import { GET_PIPELINE_PROGRESS } from '@/pipeline/graphql/queries/getPipelineProgress'; +import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { BoardHeader } from '@/ui/board/components/BoardHeader'; import { StyledBoard } from '@/ui/board/components/StyledBoard'; import { BoardColumnIdContext } from '@/ui/board/contexts/BoardColumnIdContext'; import { SelectedSortType } from '@/ui/filter-n-sort/types/interface'; import { IconList } from '@/ui/icon'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { @@ -22,6 +25,7 @@ import { useUpdateOnePipelineProgressStageMutation, } from '~/generated/graphql'; +import { useCurrentCardSelected } from '../hooks/useCurrentCardSelected'; import { useSetCardSelected } from '../hooks/useSetCardSelected'; import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds'; import { boardColumnsState } from '../states/boardColumnsState'; @@ -54,6 +58,8 @@ export function EntityBoard({ const [updatePipelineProgressStage] = useUpdateOnePipelineProgressStageMutation(); + const { unselectAllActiveCards } = useCurrentCardSelected(); + const updatePipelineProgressStageInDB = useCallback( async ( pipelineProgressId: NonNullable, @@ -70,6 +76,11 @@ export function EntityBoard({ [updatePipelineProgressStage], ); + useListenClickOutsideByClassName({ + className: 'entity-board-card', + callback: unselectAllActiveCards, + }); + const updateBoardCardIds = useUpdateBoardCardIds(); const onDragEnd: OnDragEndResponder = useCallback( @@ -106,6 +117,12 @@ export function EntityBoard({ const boardRef = useRef(null); + useScopedHotkeys( + 'escape', + unselectAllActiveCards, + PageHotkeyScope.OpportunitiesPage, + ); + return (boardColumns?.length ?? 0) > 0 ? ( (selected: boolean) => { if (!currentCardId) return; set(isCardSelectedFamilyState(currentCardId), selected); - set(actionBarOpenState, true); + set(actionBarOpenState, selected); + + if (selected) { + setActiveCardIds((prevActiveCardIds) => [ + ...prevActiveCardIds, + currentCardId, + ]); + } else { + setActiveCardIds((prevActiveCardIds) => + prevActiveCardIds.filter((id) => id !== currentCardId), + ); + } }, - [currentCardId], + [currentCardId, setActiveCardIds], + ); + + const unselectAllActiveCards = useRecoilCallback( + ({ set, snapshot }) => + () => { + const activeCardIds = snapshot.getLoadable(activeCardIdsState).contents; + + activeCardIds.forEach((cardId: string) => { + set(isCardSelectedFamilyState(cardId), false); + }); + + set(activeCardIdsState, []); + set(actionBarOpenState, false); + }, + [], ); return { currentCardSelected: isCardSelected, setCurrentCardSelected, + unselectAllActiveCards, }; } diff --git a/front/src/modules/ui/board/hooks/useSetCardSelected.ts b/front/src/modules/ui/board/hooks/useSetCardSelected.ts index afeba3e40..e279cc35a 100644 --- a/front/src/modules/ui/board/hooks/useSetCardSelected.ts +++ b/front/src/modules/ui/board/hooks/useSetCardSelected.ts @@ -2,13 +2,28 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState'; +import { activeCardIdsState } from '../states/activeCardIdsState'; import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState'; export function useSetCardSelected() { const setActionBarOpenState = useSetRecoilState(actionBarOpenState); - return useRecoilCallback(({ set }) => (cardId: string, selected: boolean) => { - set(isCardSelectedFamilyState(cardId), selected); - setActionBarOpenState(true); - }); + return useRecoilCallback( + ({ set, snapshot }) => + (cardId: string, selected: boolean) => { + const activeCardIds = snapshot.getLoadable(activeCardIdsState).contents; + + set(isCardSelectedFamilyState(cardId), selected); + setActionBarOpenState(selected || activeCardIds.length > 0); + + if (selected) { + set(activeCardIdsState, [...activeCardIds, cardId]); + } else { + set( + activeCardIdsState, + activeCardIds.filter((id: string) => id !== cardId), + ); + } + }, + ); } diff --git a/front/src/modules/ui/board/states/activeCardIdsState.ts b/front/src/modules/ui/board/states/activeCardIdsState.ts new file mode 100644 index 000000000..2221ab10c --- /dev/null +++ b/front/src/modules/ui/board/states/activeCardIdsState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const activeCardIdsState = atom({ + key: 'activeCardIdsState', + default: [], +}); diff --git a/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts b/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts index e6160059d..d3c3b7928 100644 --- a/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts +++ b/front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts @@ -76,3 +76,38 @@ export function useListenClickOutside({ }; }, [refs, callback, mode]); } + +export const useListenClickOutsideByClassName = ({ + className, + callback, +}: { + className: string; + callback: () => void; +}) => { + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const clickedElement = event.target as HTMLElement; + let isClickedInside = false; + let currentElement: HTMLElement | null = clickedElement; + + // Check if the clicked element or any of its parent elements have the specified class + while (currentElement) { + if (currentElement.classList.contains(className)) { + isClickedInside = true; + break; + } + currentElement = currentElement.parentElement; + } + + if (!isClickedInside) { + callback(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [callback, className]); +};