From b76f01d930dfc8241457556f436e943eeb3c17ef Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Thu, 10 Aug 2023 21:30:25 +0200 Subject: [PATCH] - refactor context menu and action bar into seperate components - fix styling context menu --- ...leActionBarButtonCreateActivityCompany.tsx | 13 ++-- ...bleActionBarButtonDeleteCompanies copy.tsx | 54 ++++++++++++++++ ...extMenuEntryCreateActivityCompany copy.tsx | 29 +++++++++ ...bleActionBarButtonCreateActivityPeople.tsx | 13 ++-- ...ableActionContextMenuEntryDeletePeople.tsx | 53 +++++++++++++++ ...leContextMenuEntryCreateActivityPeople.tsx | 29 +++++++++ .../ui/action-bar/components/ActionBar.tsx | 40 +----------- .../context-menu/components/ContextMenu.tsx | 64 +++++++++++++++++++ .../types/PositionType.ts | 0 .../TableActionBarButtonOpenComments.tsx | 17 ----- .../TableActionBarButtonOpenTasks.tsx | 17 ----- .../components/EntityTableContextMenu.tsx | 16 +++++ .../EntityTableContextMenuEntry.tsx | 58 +++++++++++++++++ .../table/states/contextMenuPositionState.ts | 2 +- front/src/pages/companies/Companies.tsx | 7 ++ front/src/pages/people/People.tsx | 7 ++ 16 files changed, 337 insertions(+), 82 deletions(-) create mode 100644 front/src/modules/companies/table/components/TableActionBarButtonDeleteCompanies copy.tsx create mode 100644 front/src/modules/companies/table/components/TableContextMenuEntryCreateActivityCompany copy.tsx create mode 100644 front/src/modules/people/table/components/TableActionContextMenuEntryDeletePeople.tsx create mode 100644 front/src/modules/people/table/components/TableContextMenuEntryCreateActivityPeople.tsx create mode 100644 front/src/modules/ui/context-menu/components/ContextMenu.tsx rename front/src/modules/ui/{action-bar => context-menu}/types/PositionType.ts (100%) delete mode 100644 front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenComments.tsx delete mode 100644 front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenTasks.tsx create mode 100644 front/src/modules/ui/table/context-menu/components/EntityTableContextMenu.tsx create mode 100644 front/src/modules/ui/table/context-menu/components/EntityTableContextMenuEntry.tsx diff --git a/front/src/modules/companies/table/components/TableActionBarButtonCreateActivityCompany.tsx b/front/src/modules/companies/table/components/TableActionBarButtonCreateActivityCompany.tsx index cfb5eefdad..769690cf09 100644 --- a/front/src/modules/companies/table/components/TableActionBarButtonCreateActivityCompany.tsx +++ b/front/src/modules/companies/table/components/TableActionBarButtonCreateActivityCompany.tsx @@ -1,6 +1,7 @@ +import { IconCheckbox, IconNotes } from '@tabler/icons-react'; + import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; -import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments'; -import { TableActionBarButtonToggleTasks } from '@/ui/table/action-bar/components/TableActionBarButtonOpenTasks'; +import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton'; import { ActivityType, CommentableType } from '~/generated/graphql'; export function TableActionBarButtonCreateActivityCompany() { @@ -13,10 +14,14 @@ export function TableActionBarButtonCreateActivityCompany() { return ( <> - } onClick={() => handleButtonClick(ActivityType.Note)} /> - } onClick={() => handleButtonClick(ActivityType.Task)} /> diff --git a/front/src/modules/companies/table/components/TableActionBarButtonDeleteCompanies copy.tsx b/front/src/modules/companies/table/components/TableActionBarButtonDeleteCompanies copy.tsx new file mode 100644 index 0000000000..04469aa2ed --- /dev/null +++ b/front/src/modules/companies/table/components/TableActionBarButtonDeleteCompanies copy.tsx @@ -0,0 +1,54 @@ +import { getOperationName } from '@apollo/client/utilities'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { GET_PIPELINES } from '@/pipeline/queries'; +import { IconTrash } from '@/ui/icon/index'; +import { EntityTableContextMenuEntry } from '@/ui/table/context-menu/components/EntityTableContextMenuEntry'; +import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; +import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; +import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; +import { useDeleteManyCompaniesMutation } from '~/generated/graphql'; + +export function TableContextMenuEntryDeleteCompanies() { + const selectedRowIds = useRecoilValue(selectedRowIdsSelector); + + const resetRowSelection = useResetTableRowSelection(); + + const [deleteCompanies] = useDeleteManyCompaniesMutation({ + refetchQueries: [getOperationName(GET_PIPELINES) ?? ''], + }); + + const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState); + + async function handleDeleteClick() { + const rowIdsToDelete = selectedRowIds; + + resetRowSelection(); + + await deleteCompanies({ + variables: { + ids: rowIdsToDelete, + }, + optimisticResponse: { + __typename: 'Mutation', + deleteManyCompany: { + count: rowIdsToDelete.length, + }, + }, + update: () => { + setTableRowIds( + tableRowIds.filter((id) => !rowIdsToDelete.includes(id)), + ); + }, + }); + } + + return ( + } + type="warning" + onClick={handleDeleteClick} + /> + ); +} diff --git a/front/src/modules/companies/table/components/TableContextMenuEntryCreateActivityCompany copy.tsx b/front/src/modules/companies/table/components/TableContextMenuEntryCreateActivityCompany copy.tsx new file mode 100644 index 0000000000..76d1fd4f9a --- /dev/null +++ b/front/src/modules/companies/table/components/TableContextMenuEntryCreateActivityCompany copy.tsx @@ -0,0 +1,29 @@ +import { IconCheckbox, IconNotes } from '@tabler/icons-react'; + +import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; +import { EntityTableContextMenuEntry } from '@/ui/table/context-menu/components/EntityTableContextMenuEntry'; +import { ActivityType, CommentableType } from '~/generated/graphql'; + +export function TableContextMenuEntryCreateActivityCompany() { + const openCreateActivityRightDrawer = + useOpenCreateActivityDrawerForSelectedRowIds(); + + async function handleButtonClick(type: ActivityType) { + openCreateActivityRightDrawer(type, CommentableType.Company); + } + + return ( + <> + } + onClick={() => handleButtonClick(ActivityType.Note)} + /> + } + onClick={() => handleButtonClick(ActivityType.Task)} + /> + + ); +} diff --git a/front/src/modules/people/table/components/TableActionBarButtonCreateActivityPeople.tsx b/front/src/modules/people/table/components/TableActionBarButtonCreateActivityPeople.tsx index 9d8e271ca8..2a9f065ce5 100644 --- a/front/src/modules/people/table/components/TableActionBarButtonCreateActivityPeople.tsx +++ b/front/src/modules/people/table/components/TableActionBarButtonCreateActivityPeople.tsx @@ -1,6 +1,7 @@ +import { IconCheckbox, IconNotes } from '@tabler/icons-react'; + import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; -import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments'; -import { TableActionBarButtonToggleTasks } from '@/ui/table/action-bar/components/TableActionBarButtonOpenTasks'; +import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton'; import { ActivityType, CommentableType } from '~/generated/graphql'; export function TableActionBarButtonCreateActivityPeople() { @@ -13,10 +14,14 @@ export function TableActionBarButtonCreateActivityPeople() { return ( <> - } onClick={() => handleButtonClick(ActivityType.Note)} /> - } onClick={() => handleButtonClick(ActivityType.Task)} /> diff --git a/front/src/modules/people/table/components/TableActionContextMenuEntryDeletePeople.tsx b/front/src/modules/people/table/components/TableActionContextMenuEntryDeletePeople.tsx new file mode 100644 index 0000000000..b03edf0b6d --- /dev/null +++ b/front/src/modules/people/table/components/TableActionContextMenuEntryDeletePeople.tsx @@ -0,0 +1,53 @@ +import { getOperationName } from '@apollo/client/utilities'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { GET_PEOPLE } from '@/people/queries'; +import { IconTrash } from '@/ui/icon/index'; +import { EntityTableContextMenuEntry } from '@/ui/table/context-menu/components/EntityTableContextMenuEntry'; +import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; +import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; +import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; +import { useDeleteManyPersonMutation } from '~/generated/graphql'; + +export function TableContextMenuEntryDeletePeople() { + const selectedRowIds = useRecoilValue(selectedRowIdsSelector); + const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState); + + const resetRowSelection = useResetTableRowSelection(); + + const [deleteManyPerson] = useDeleteManyPersonMutation({ + refetchQueries: [getOperationName(GET_PEOPLE) ?? ''], + }); + + async function handleDeleteClick() { + const rowIdsToDelete = selectedRowIds; + + resetRowSelection(); + + await deleteManyPerson({ + variables: { + ids: rowIdsToDelete, + }, + optimisticResponse: { + __typename: 'Mutation', + deleteManyPerson: { + count: rowIdsToDelete.length, + }, + }, + update: () => { + setTableRowIds( + tableRowIds.filter((id) => !rowIdsToDelete.includes(id)), + ); + }, + }); + } + + return ( + } + type="warning" + onClick={handleDeleteClick} + /> + ); +} diff --git a/front/src/modules/people/table/components/TableContextMenuEntryCreateActivityPeople.tsx b/front/src/modules/people/table/components/TableContextMenuEntryCreateActivityPeople.tsx new file mode 100644 index 0000000000..8aa70de10f --- /dev/null +++ b/front/src/modules/people/table/components/TableContextMenuEntryCreateActivityPeople.tsx @@ -0,0 +1,29 @@ +import { IconCheckbox, IconNotes } from '@tabler/icons-react'; + +import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; +import { EntityTableContextMenuEntry } from '@/ui/table/context-menu/components/EntityTableContextMenuEntry'; +import { ActivityType, CommentableType } from '~/generated/graphql'; + +export function TableContextMenuEntryCreateActivityPeople() { + const openCreateActivityRightDrawer = + useOpenCreateActivityDrawerForSelectedRowIds(); + + async function handleButtonClick(type: ActivityType) { + openCreateActivityRightDrawer(type, CommentableType.Person); + } + + return ( + <> + } + onClick={() => handleButtonClick(ActivityType.Note)} + /> + } + onClick={() => handleButtonClick(ActivityType.Task)} + /> + + ); +} diff --git a/front/src/modules/ui/action-bar/components/ActionBar.tsx b/front/src/modules/ui/action-bar/components/ActionBar.tsx index 72f7d8361c..8de0c70de2 100644 --- a/front/src/modules/ui/action-bar/components/ActionBar.tsx +++ b/front/src/modules/ui/action-bar/components/ActionBar.tsx @@ -4,42 +4,11 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { contextMenuPositionState } from '@/ui/table/states/contextMenuPositionState'; -import { PositionType } from '../types/PositionType'; - type OwnProps = { children: React.ReactNode | React.ReactNode[]; selectedIds: string[]; }; -type StyledContainerProps = { - position: PositionType; -}; - -const StyledContainerContextMenu = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.md}; - bottom: ${(props) => (props.position.x ? 'auto' : '38px')}; - box-shadow: ${({ theme }) => theme.boxShadow.strong}; - - left: ${(props) => (props.position.x ? `${props.position.x}px` : '50%')}; - padding-bottom: ${({ theme }) => theme.spacing(1)}; - padding-left: ${({ theme }) => theme.spacing(1)}; - padding-right: ${({ theme }) => theme.spacing(1)}; - padding-top: ${({ theme }) => theme.spacing(1)}; - position: ${(props) => (props.position.x ? 'fixed' : 'absolute')}; - top: ${(props) => (props.position.y ? `${props.position.y}px` : 'auto')}; - - transform: translateX(-50%); - width: 160px; - z-index: 1; - - div { - justify-content: left; - } -`; - const StyledContainerActionBar = styled.div` align-items: center; background: ${({ theme }) => theme.background.secondary}; @@ -79,16 +48,9 @@ export function ActionBar({ children, selectedIds }: OwnProps) { }; }, [setContextMenuPosition]); - if (selectedIds.length === 0) { + if (selectedIds.length === 0 || position.x || position.y) { return null; } - if (position.x && position.y) { - return ( - - {children} - - ); - } return ( {children} diff --git a/front/src/modules/ui/context-menu/components/ContextMenu.tsx b/front/src/modules/ui/context-menu/components/ContextMenu.tsx new file mode 100644 index 0000000000..b9867e5b1e --- /dev/null +++ b/front/src/modules/ui/context-menu/components/ContextMenu.tsx @@ -0,0 +1,64 @@ +import React, { useEffect } from 'react'; +import styled from '@emotion/styled'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; + +import { contextMenuPositionState } from '@/ui/table/states/contextMenuPositionState'; + +import { PositionType } from '../types/PositionType'; + +type OwnProps = { + children: React.ReactNode | React.ReactNode[]; + selectedIds: string[]; +}; + +type StyledContainerProps = { + position: PositionType; +}; + +const StyledContainerContextMenu = styled.div` + align-items: flex-start; + background: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + box-shadow: ${({ theme }) => theme.boxShadow.strong}; + display: flex; + flex-direction: column; + gap: 1px; + + left: ${(props) => `${props.position.x}px`}; + position: fixed; + top: ${(props) => `${props.position.y}px`}; + + transform: translateX(-50%); + width: 160px; + z-index: 1; +`; + +export function ContextMenu({ children, selectedIds }: OwnProps) { + const position = useRecoilValue(contextMenuPositionState); + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (!(event.target as HTMLElement).closest('.action-bar')) { + setContextMenuPosition({ x: null, y: null }); + } + } + + document.addEventListener('mousedown', handleClickOutside); + + // Cleanup the event listener when the component unmounts + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [setContextMenuPosition]); + + if (selectedIds.length === 0 || (!position.x && !position.y)) { + return null; + } + return ( + + {children} + + ); +} diff --git a/front/src/modules/ui/action-bar/types/PositionType.ts b/front/src/modules/ui/context-menu/types/PositionType.ts similarity index 100% rename from front/src/modules/ui/action-bar/types/PositionType.ts rename to front/src/modules/ui/context-menu/types/PositionType.ts diff --git a/front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenComments.tsx b/front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenComments.tsx deleted file mode 100644 index 8da8db3288..0000000000 --- a/front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenComments.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { IconNotes } from '@/ui/icon/index'; - -import { EntityTableActionBarButton } from './EntityTableActionBarButton'; - -type OwnProps = { - onClick: () => void; -}; - -export function TableActionBarButtonToggleComments({ onClick }: OwnProps) { - return ( - } - onClick={onClick} - /> - ); -} diff --git a/front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenTasks.tsx b/front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenTasks.tsx deleted file mode 100644 index b407ec239b..0000000000 --- a/front/src/modules/ui/table/action-bar/components/TableActionBarButtonOpenTasks.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { IconCheckbox } from '@/ui/icon/index'; - -import { EntityTableActionBarButton } from './EntityTableActionBarButton'; - -type OwnProps = { - onClick: () => void; -}; - -export function TableActionBarButtonToggleTasks({ onClick }: OwnProps) { - return ( - } - onClick={onClick} - /> - ); -} diff --git a/front/src/modules/ui/table/context-menu/components/EntityTableContextMenu.tsx b/front/src/modules/ui/table/context-menu/components/EntityTableContextMenu.tsx new file mode 100644 index 0000000000..846e885604 --- /dev/null +++ b/front/src/modules/ui/table/context-menu/components/EntityTableContextMenu.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { useRecoilValue } from 'recoil'; + +import { ContextMenu } from '@/ui/context-menu/components/ContextMenu'; + +import { selectedRowIdsSelector } from '../../states/selectedRowIdsSelector'; + +type OwnProps = { + children: React.ReactNode | React.ReactNode[]; +}; + +export function EntityTableContextMenu({ children }: OwnProps) { + const selectedRowIds = useRecoilValue(selectedRowIdsSelector); + + return {children}; +} diff --git a/front/src/modules/ui/table/context-menu/components/EntityTableContextMenuEntry.tsx b/front/src/modules/ui/table/context-menu/components/EntityTableContextMenuEntry.tsx new file mode 100644 index 0000000000..3adfb049e4 --- /dev/null +++ b/front/src/modules/ui/table/context-menu/components/EntityTableContextMenuEntry.tsx @@ -0,0 +1,58 @@ +import { ReactNode } from 'react'; +import styled from '@emotion/styled'; + +type OwnProps = { + icon: ReactNode; + label: string; + type?: 'standard' | 'warning'; + onClick: () => void; +}; + +type StyledButtonProps = { + type: 'standard' | 'warning'; +}; + +const StyledButton = styled.div` + align-items: center; + align-self: stretch; + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${(props) => + props.type === 'warning' + ? props.theme.color.red + : props.theme.font.color.secondary}; + cursor: pointer; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + + height: 32px; + padding-left: ${({ theme }) => theme.spacing(1)}; + padding-right: ${({ theme }) => theme.spacing(1)}; + transition: background 0.1s ease; + user-select: none; + + &:hover { + background: ${({ theme, type }) => + type === 'warning' + ? theme.tag.background.red + : theme.background.tertiary}; + } +`; + +const StyledButtonLabel = styled.div` + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +export function EntityTableContextMenuEntry({ + label, + icon, + type = 'standard', + onClick, +}: OwnProps) { + return ( + + {icon} + {label} + + ); +} diff --git a/front/src/modules/ui/table/states/contextMenuPositionState.ts b/front/src/modules/ui/table/states/contextMenuPositionState.ts index 1832437b5f..7cd20a1c67 100644 --- a/front/src/modules/ui/table/states/contextMenuPositionState.ts +++ b/front/src/modules/ui/table/states/contextMenuPositionState.ts @@ -1,6 +1,6 @@ import { atom } from 'recoil'; -import { PositionType } from '@/ui/action-bar/types/PositionType'; +import { PositionType } from '@/ui/context-menu/types/PositionType'; export const contextMenuPositionState = atom({ key: 'contextMenuPositionState', diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index 79d04fd694..95741730cc 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -7,10 +7,13 @@ import { v4 } from 'uuid'; import { CompanyTable } from '@/companies/table/components/CompanyTable'; import { TableActionBarButtonCreateActivityCompany } from '@/companies/table/components/TableActionBarButtonCreateActivityCompany'; import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies'; +import { TableContextMenuEntryDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies copy'; +import { TableContextMenuEntryCreateActivityCompany } from '@/companies/table/components/TableContextMenuEntryCreateActivityCompany copy'; import { SEARCH_COMPANY_QUERY } from '@/search/queries/search'; import { IconBuildingSkyscraper } from '@/ui/icon'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar'; +import { EntityTableContextMenu } from '@/ui/table/context-menu/components/EntityTableContextMenu'; import { TableContext } from '@/ui/table/states/TableContext'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; @@ -72,6 +75,10 @@ export function Companies() { + + + + diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 4ed08ad2ca..127b6f7369 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -6,9 +6,12 @@ import { v4 } from 'uuid'; import { PeopleTable } from '@/people/table/components/PeopleTable'; import { TableActionBarButtonCreateActivityPeople } from '@/people/table/components/TableActionBarButtonCreateActivityPeople'; import { TableActionBarButtonDeletePeople } from '@/people/table/components/TableActionBarButtonDeletePeople'; +import { TableContextMenuEntryDeletePeople } from '@/people/table/components/TableActionContextMenuEntryDeletePeople'; +import { TableContextMenuEntryCreateActivityPeople } from '@/people/table/components/TableContextMenuEntryCreateActivityPeople'; import { IconUser } from '@/ui/icon'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar'; +import { EntityTableContextMenu } from '@/ui/table/context-menu/components/EntityTableContextMenu'; import { TableContext } from '@/ui/table/states/TableContext'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; @@ -67,6 +70,10 @@ export function People() { + + + + );