Menu consistent for table view (#4392)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Fixes https://github.com/mattermost/focalboard/issues/4281
This commit is contained in:
Rajat Dabade 2023-01-04 15:15:09 +05:30 committed by GitHub
parent 3e89a88d7e
commit 1c9ab89085
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 608 additions and 179 deletions

View File

@ -167,6 +167,7 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
<CardActionsMenuIcon/>
<CardActionsMenu
cardId={card.id}
boardId={card.boardId}
onClickDelete={() => openConfirmationDialogBox(card)}
onClickDuplicate={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})

View File

@ -48,6 +48,7 @@ describe('components/cardActionsMenu', () => {
<ReduxProvider store={store}>
<CardActionsMenu
cardId='123'
boardId='345'
onClickDelete={jest.fn()}
/>
</ReduxProvider>,
@ -64,6 +65,7 @@ describe('components/cardActionsMenu', () => {
<ReduxProvider store={store}>
<CardActionsMenu
cardId='123'
boardId='345'
onClickDelete={jest.fn()}
onClickDuplicate={jest.fn()}
/>
@ -81,6 +83,7 @@ describe('components/cardActionsMenu', () => {
<ReduxProvider store={store}>
<CardActionsMenu
cardId='123'
boardId='345'
onClickDelete={jest.fn()}
>
<React.Fragment>

View File

@ -15,9 +15,11 @@ import {sendFlashMessage} from '../flashMessages'
import {IUser} from '../../user'
import {getMe} from '../../store/users'
import {useAppSelector} from '../../store/hooks'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
type Props = {
cardId: string
boardId: string
onClickDelete: () => void
onClickDuplicate?: () => void
children?: ReactNode
@ -29,6 +31,18 @@ export const CardActionsMenu = (props: Props): JSX.Element => {
const me = useAppSelector<IUser|null>(getMe)
const intl = useIntl()
const handleDeleteCard = () => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteCard, {board: props.boardId, card: props.cardId})
props.onClickDelete()
}
const handleDuplicateCard = () => {
if (props.onClickDuplicate) {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: props.boardId, card: props.cardId})
props.onClickDuplicate()
}
}
return (
<Menu position='left'>
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
@ -36,14 +50,14 @@ export const CardActionsMenu = (props: Props): JSX.Element => {
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'CardActionsMenu.delete', defaultMessage: 'Delete'})}
onClick={props.onClickDelete}
onClick={handleDeleteCard}
/>
{props.onClickDuplicate &&
<Menu.Text
icon={<DuplicateIcon/>}
id='duplicate'
name={intl.formatMessage({id: 'CardActionsMenu.duplicate', defaultMessage: 'Duplicate'})}
onClick={props.onClickDuplicate}
onClick={handleDuplicateCard}
/>}
</BoardPermissionGate>
{me?.id !== 'single-user' &&

View File

@ -120,6 +120,7 @@ const CardDialog = (props: Props): JSX.Element => {
const menu = (
<CardActionsMenu
cardId={props.cardId}
boardId={board.id}
onClickDelete={handleDeleteButtonOnClick}
>
{!isTemplate &&

View File

@ -92,6 +92,7 @@ const GalleryCard = (props: Props) => {
<CardActionsMenuIcon/>
<CardActionsMenu
cardId={card!.id}
boardId={card!.boardId}
onClickDelete={() => setShowConfirmationDialogBox(true)}
onClickDuplicate={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})

View File

@ -105,6 +105,7 @@ const KanbanCard = (props: Props) => {
<CardActionsMenuIcon/>
<CardActionsMenu
cardId={card!.id}
boardId={card!.boardId}
onClickDelete={handleDeleteButtonOnClick}
onClickDuplicate={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,13 @@
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import {render, screen, waitFor} from '@testing-library/react'
import {render, screen} from '@testing-library/react'
import configureStore from 'redux-mock-store'
import '@testing-library/jest-dom'
import userEvents from '@testing-library/user-event'
import 'isomorphic-fetch'
import {mocked} from 'jest-mock'
import {TestBlockFactory} from '../../test/testBlockFactory'
import {FetchMock} from '../../test/fetchMock'
@ -20,6 +21,8 @@ import {Utils, IDType} from '../../utils'
import {wrapDNDIntl} from '../../testUtils'
import Mutator from '../../mutator'
import Table from './table'
global.fetch = FetchMock.fn
@ -28,6 +31,11 @@ beforeEach(() => {
FetchMock.fn.mockReset()
})
jest.mock('../../mutator')
jest.mock('../../utils')
jest.mock('../../telemetry/telemetryClient')
const mockedMutator = mocked(Mutator, true)
describe('components/table/Table', () => {
const board = TestBlockFactory.createBoard()
const view = TestBlockFactory.createBoardView(board)
@ -675,8 +683,67 @@ describe('components/table/Table extended', () => {
userEvents.click(deleteBtn)
const dailogDeleteBtn = screen.getByRole('button', {name: 'Delete'})
userEvents.click(dailogDeleteBtn)
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(`http://localhost/api/v2/boards/${board.id}/blocks/${card1.id}`, {headers: {Accept: 'application/json', Authorization: '', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'}, method: 'DELETE'})
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
})
test('should have Duplicate Button', async () => {
const board = TestBlockFactory.createBoard()
const modifiedById = Utils.createGuid(IDType.User)
board.cardProperties.push({
id: modifiedById,
name: 'Last Modified By',
type: 'updatedBy',
options: [],
})
const card1 = TestBlockFactory.createCard(board)
card1.title = 'card1'
const card2 = TestBlockFactory.createCard(board)
card2.title = 'card2'
const view = TestBlockFactory.createBoardView(board)
view.fields.viewType = 'table'
view.fields.groupById = undefined
view.fields.visiblePropertyIds = ['property1', 'property2', modifiedById]
const mockStore = configureStore([])
const store = mockStore({
...state,
cards: {
cards: {
[card1.id]: card1,
[card2.id]: card2,
},
},
})
const component = wrapDNDIntl(
<ReduxProvider store={store}>
<Table
board={board}
activeView={view}
visibleGroups={[]}
cards={[card1, card2]}
views={[view]}
selectedCardIds={[]}
readonly={false}
cardIdToFocusOnRender=''
showCard={jest.fn()}
addCard={jest.fn()}
onCardClicked={jest.fn()}
hiddenCardsCount={0}
showHiddenCardCountNotification={jest.fn()}
/>
</ReduxProvider>,
)
const {getByTitle, getByRole, getAllByTitle, container} = render(component)
const card1Name = getByTitle(card1.title)
userEvents.hover(card1Name)
const menuBtn = getAllByTitle('MenuBtn')
userEvents.click(menuBtn[0])
const duplicateBtn = getByRole('button', {name: 'Duplicate'})
expect(duplicateBtn).not.toBe(null)
userEvents.click(duplicateBtn)
expect(mockedMutator.duplicateCard).toBeCalledTimes(1)
expect(container).toMatchSnapshot()
})
})

View File

@ -14,15 +14,14 @@ import {useSortable} from '../../hooks/sortable'
import {Utils} from '../../utils'
import PropertyValueElement from '../propertyValueElement'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import IconButton from '../../widgets/buttons/iconButton'
import CompassIcon from '../../widgets/icons/compassIcon'
import OptionsIcon from '../../widgets/icons/options'
import DeleteIcon from '../../widgets/icons/delete'
import Tooltip from '../../widgets/tooltip'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
import CardActionsMenu from '../cardActionsMenu/cardActionsMenu'
import {useColumnResize} from './tableColumnResizeContext'
@ -39,7 +38,7 @@ type Props = {
isSelected: boolean
focusOnMount: boolean
isLastCard: boolean
showCard: (cardId: string) => void
showCard: (cardId?: string) => void
readonly: boolean
addCard: (groupByOptionId?: string) => Promise<void>
onClick?: (e: React.MouseEvent<HTMLDivElement>, card: Card) => void
@ -184,14 +183,27 @@ const TableRow = (props: Props) => {
icon={<OptionsIcon/>}
/>
</Tooltip>
<Menu>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'TableRow.delete', defaultMessage: 'Delete'})}
onClick={handleDeleteButtonOnClick}
/>
</Menu>
<CardActionsMenu
cardId={card.id}
boardId={card.boardId}
onClickDelete={handleDeleteButtonOnClick}
onClickDuplicate={() => {
mutator.duplicateCard(
card.id,
board.id,
false,
intl.formatMessage({id: 'TableRow.DuplicateCard', defaultMessage: 'duplicate card'}),
false,
{},
async (newCardId) => {
props.showCard(newCardId)
},
async () => {
props.showCard(undefined)
},
)
}}
/>
</MenuWrapper>
)}