1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-26 20:53:55 +02:00

Refactor "..." menu into component & add to Calendar view (#3321)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Paul Esch-Laurent 2022-07-07 10:44:28 -05:00 committed by GitHub
parent 46fdbf9048
commit 3bbecc8117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 617 additions and 155 deletions

View File

@ -63,6 +63,11 @@
"Calculations.Options.range.label": "Range",
"Calculations.Options.sum.displayName": "Sum",
"Calculations.Options.sum.label": "Sum",
"CalendarCard.untitled": "Untitled",
"CardActionsMenu.copiedLink": "Copied!",
"CardActionsMenu.copyLink": "Copy link",
"CardActionsMenu.delete": "Delete",
"CardActionsMenu.duplicate": "Duplicate",
"CardBadges.title-checkboxes": "Checkboxes",
"CardBadges.title-comments": "Comments",
"CardBadges.title-description": "This card has a description",
@ -87,8 +92,6 @@
"CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!",
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
"CardDetial.limited-link": "Learn more about our plans.",
"CardDialog.copiedLink": "Copied!",
"CardDialog.copyLink": "Copy link",
"CardDialog.delete-confirmation-dialog-button-text": "Delete",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!",
"CardDialog.editing-template": "You're editing a template.",
@ -144,17 +147,9 @@
"FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.",
"FindBoardsDialog.SubTitle": "Type to find a board. Use <b>UP/DOWN</b> to browse. <b>ENTER</b> to select, <b>ESC</b> to dismiss",
"FindBoardsDialog.Title": "Find Boards",
"GalleryCard.copiedLink": "Copied!",
"GalleryCard.copyLink": "Copy link",
"GalleryCard.delete": "Delete",
"GalleryCard.duplicate": "Duplicate",
"GroupBy.hideEmptyGroups": "Hide {count} empty groups",
"GroupBy.showHiddenGroups": "Show {count} hidden groups",
"GroupBy.ungroup": "Ungroup",
"KanbanCard.copiedLink": "Copied!",
"KanbanCard.copyLink": "Copy link",
"KanbanCard.delete": "Delete",
"KanbanCard.duplicate": "Duplicate",
"KanbanCard.untitled": "Untitled",
"Mutator.new-board-from-template": "new board from template",
"Mutator.new-card-from-template": "new card from template",

View File

@ -727,6 +727,20 @@ exports[`components/calendar/toolbar return calendar, no date property 1`] = `
<div
class="EventContent"
>
<div
aria-label="menuwrapper"
class="MenuWrapper optionsMenu"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<div
class="octo-icontitle"
>
@ -2987,6 +3001,20 @@ exports[`components/calendar/toolbar return calendar, with date property not set
<div
class="EventContent"
>
<div
aria-label="menuwrapper"
class="MenuWrapper optionsMenu"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<div
class="octo-icontitle"
>
@ -5945,6 +5973,20 @@ exports[`components/calendar/toolbar return calendar, with date property set 1`]
<div
class="EventContent"
>
<div
aria-label="menuwrapper"
class="MenuWrapper optionsMenu"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<div
class="octo-icontitle"
>
@ -7513,6 +7555,20 @@ exports[`components/calendar/toolbar return calendar, without permissions 1`] =
<div
class="EventContent"
>
<div
aria-label="menuwrapper"
class="MenuWrapper optionsMenu"
role="button"
>
<button
class="IconButton"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
<div
class="octo-icontitle"
>

View File

@ -22,6 +22,11 @@ import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
import CardBadges from '../cardBadges'
import './fullcalendar.scss'
import MenuWrapper from '../../widgets/menuWrapper'
import IconButton from '../../widgets/buttons/iconButton'
import CardActionsMenu from '../cardActionsMenu/cardActionsMenu'
import OptionsIcon from '../../widgets/icons/options'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
const oneDay = 60 * 60 * 24 * 1000
@ -121,17 +126,33 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
const renderEventContent = (eventProps: EventContentArg): JSX.Element|null => {
const {event} = eventProps
const card = cards.find((o) => o.id === event.id) || cards[0]
return (
<div
className='EventContent'
onClick={() => props.showCard(event.id)}
>
{!props.readonly &&
<MenuWrapper
className='optionsMenu'
stopPropagationOnToggle={true}
>
<IconButton icon={<OptionsIcon/>}/>
<CardActionsMenu
cardId={card.id}
onClickDelete={() => mutator.deleteBlock(card, 'delete card')}
onClickDuplicate={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})
mutator.duplicateCard(card.id, board.id)
}}
/>
</MenuWrapper>}
<div className='octo-icontitle'>
{ event.extendedProps.icon ? <div className='octo-icon'>{event.extendedProps.icon}</div> : undefined }
<div
className='fc-event-title'
key='__title'
>{event.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}</div>
>{event.title || intl.formatMessage({id: 'CalendarCard.untitled', defaultMessage: 'Untitled'})}</div>
</div>
{visiblePropertyTemplates.map((template) => (
<Tooltip
@ -141,14 +162,14 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
<PropertyValueElement
board={board}
readOnly={true}
card={cards.find((o) => o.id === event.id) || cards[0]}
card={card}
propertyTemplate={template}
showEmptyPlaceholder={false}
/>
</Tooltip>
))}
{visibleBadges &&
<CardBadges card={cards.find((o) => o.id === event.id) || cards[0]}/> }
<CardBadges card={card}/> }
</div>
)
}

View File

@ -10,6 +10,10 @@
&:hover {
background-color: unset;
.optionsMenu {
display: block;
}
}
}
@ -19,6 +23,20 @@
align-items: flex-start;
}
.optionsMenu {
background-color: rgb(var(--center-channel-bg-rgb));
border-radius: var(--default-rad);
display: none;
position: absolute;
right: 0;
top: 0;
z-index: 30;
.IconButton {
background-color: rgba(var(--center-channel-color-rgb), 0.13);
}
}
.octo-tooltip {
display: flex;
max-width: 100%;
@ -180,7 +198,6 @@
background-color: rgb(var(--center-channel-bg-rgb));
box-shadow: var(--elevation-1);
margin: 0 8px 10px;
overflow: hidden;
padding: 4px 6px;
&:hover::before {

View File

@ -0,0 +1,300 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/cardActionsMenu should match snapshot 1`] = `
<div>
<div
class="Menu noselect left "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="Delete"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</div>
<div
class="menu-name"
>
Delete
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Copy link"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
</div>
<div
class="menu-name"
>
Copy link
</div>
<div
class="noicon"
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-name"
>
Cancel
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/cardActionsMenu should match snapshot w/ children prop 1`] = `
<div>
<div
class="Menu noselect left "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="Delete"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</div>
<div
class="menu-name"
>
Delete
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Copy link"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
</div>
<div
class="menu-name"
>
Copy link
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
Test.
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-name"
>
Cancel
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/cardActionsMenu should match snapshot w/ onClickDuplicate prop 1`] = `
<div>
<div
class="Menu noselect left "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="Delete"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
/>
</div>
<div
class="menu-name"
>
Delete
</div>
<div
class="noicon"
/>
</div>
<div
aria-label="Duplicate"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-content-copy content-copy"
/>
</div>
<div
class="menu-name"
>
Duplicate
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Copy link"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<i
class="CompassIcon icon-link-variant LinkIcon"
/>
</div>
<div
class="menu-name"
>
Copy link
</div>
<div
class="noicon"
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-name"
>
Cancel
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,96 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/jest-dom'
import {act, render} from '@testing-library/react'
import React from 'react'
import {Provider as ReduxProvider} from 'react-redux'
import { TestBlockFactory } from '../../test/testBlockFactory'
import {mockDOM, mockStateStore, wrapIntl} from '../../testUtils'
import CardActionsMenu from './cardActionsMenu'
beforeAll(() => {
mockDOM()
})
describe('components/cardActionsMenu', () => {
const board = TestBlockFactory.createBoard()
board.id = 'boardId'
const state = {
boards: {
current: board.id,
boards: {
[board.id]: board,
},
templates: [],
myBoardMemberships: {
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
},
},
teams: {
current: {id: 'team-id'},
},
users: {
me: {
id: 'user_id_1',
},
},
}
const store = mockStateStore([], state)
test('should match snapshot', async () => {
let container
await act(async () => {
const result = render(wrapIntl(
<ReduxProvider store={store}>
<CardActionsMenu
cardId='123'
onClickDelete={jest.fn()}
/>
</ReduxProvider>,
))
container = result.container
})
expect(container).toMatchSnapshot()
})
test('should match snapshot w/ onClickDuplicate prop', async () => {
let container
await act(async () => {
const result = render(wrapIntl(
<ReduxProvider store={store}>
<CardActionsMenu
cardId='123'
onClickDelete={jest.fn()}
onClickDuplicate={jest.fn()}
/>
</ReduxProvider>,
))
container = result.container
})
expect(container).toMatchSnapshot()
})
test('should match snapshot w/ children prop', async () => {
let container
await act(async () => {
const result = render(wrapIntl(
<ReduxProvider store={store}>
<CardActionsMenu
cardId='123'
onClickDelete={jest.fn()}
>
<React.Fragment>
Test.
</React.Fragment>
</CardActionsMenu>
</ReduxProvider>,
))
container = result.container
})
expect(container).toMatchSnapshot()
})
})

View File

@ -0,0 +1,71 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, { ReactNode } from 'react'
import {useIntl} from 'react-intl'
import DeleteIcon from '../../widgets/icons/delete'
import Menu from '../../widgets/menu'
import BoardPermissionGate from '../permissions/boardPermissionGate'
import DuplicateIcon from '../../widgets/icons/duplicate'
import LinkIcon from '../../widgets/icons/Link'
import {Utils} from '../../utils'
import {Permission} from '../../constants'
import {sendFlashMessage} from '../flashMessages'
import {IUser} from '../../user'
import {getMe} from '../../store/users'
import {useAppSelector} from '../../store/hooks'
type Props = {
cardId: string
onClickDelete: () => void,
onClickDuplicate?: () => void
children?: ReactNode
}
export const CardActionsMenu = (props: Props): JSX.Element => {
const {cardId} = props
const me = useAppSelector<IUser|null>(getMe)
const intl = useIntl()
return (
<Menu position='left'>
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'CardActionsMenu.delete', defaultMessage: 'Delete'})}
onClick={props.onClickDelete}
/>
{props.onClickDuplicate &&
<Menu.Text
icon={<DuplicateIcon/>}
id='duplicate'
name={intl.formatMessage({id: 'CardActionsMenu.duplicate', defaultMessage: 'Duplicate'})}
onClick={props.onClickDuplicate}
/>}
</BoardPermissionGate>
{me?.id !== 'single-user' &&
<Menu.Text
icon={<LinkIcon/>}
id='copy'
name={intl.formatMessage({id: 'CardActionsMenu.copyLink', defaultMessage: 'Copy link'})}
onClick={() => {
let cardLink = window.location.href
if (!cardLink.includes(cardId)) {
cardLink += `/${cardId}`
}
Utils.copyTextToClipboard(cardLink)
sendFlashMessage({content: intl.formatMessage({id: 'CardActionsMenu.copiedLink', defaultMessage: 'Copied!'}), severity: 'high'})
}}
/>
}
{props.children}
</Menu>
)
}
export default CardActionsMenu

View File

@ -14,8 +14,6 @@ import {useAppSelector} from '../store/hooks'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
import {Utils} from '../utils'
import CompassIcon from '../widgets/icons/compassIcon'
import DeleteIcon from '../widgets/icons/delete'
import LinkIcon from '../widgets/icons/Link'
import Menu from '../widgets/menu'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../components/confirmationDialogBox'
@ -32,9 +30,9 @@ import BoardPermissionGate from './permissions/boardPermissionGate'
import CardDetail from './cardDetail/cardDetail'
import Dialog from './dialog'
import {sendFlashMessage} from './flashMessages'
import './cardDialog.scss'
import CardActionsMenu from './cardActionsMenu/cardActionsMenu'
type Props = {
board: Board
@ -110,32 +108,10 @@ const CardDialog = (props: Props): JSX.Element => {
}
const menu = (
<Menu position='left'>
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
<Menu.Text
id='delete'
icon={<DeleteIcon/>}
name='Delete'
onClick={handleDeleteButtonOnClick}
/>
</BoardPermissionGate>
{me?.id !== 'single-user' &&
<Menu.Text
icon={<LinkIcon/>}
id='copy'
name={intl.formatMessage({id: 'CardDialog.copyLink', defaultMessage: 'Copy link'})}
onClick={() => {
let cardLink = window.location.href
if (!cardLink.includes(props.cardId)) {
cardLink += `/${props.cardId}`
}
Utils.copyTextToClipboard(cardLink)
sendFlashMessage({content: intl.formatMessage({id: 'CardDialog.copiedLink', defaultMessage: 'Copied!'}), severity: 'high'})
}}
/>
}
<CardActionsMenu
cardId={props.cardId}
onClickDelete={handleDeleteButtonOnClick}
>
{!isTemplate &&
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
<Menu.Text
@ -149,7 +125,7 @@ const CardDialog = (props: Props): JSX.Element => {
/>
</BoardPermissionGate>
}
</Menu>
</CardActionsMenu>
)
const followActionButton = (following: boolean): React.ReactNode => {

View File

@ -282,6 +282,7 @@ exports[`src/components/gallery/Gallery should match snapshot 1`] = `
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -413,6 +414,7 @@ exports[`src/components/gallery/Gallery should match snapshot without permission
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"

View File

@ -123,6 +123,7 @@ exports[`src/components/gallery/GalleryCard with a comment content should match
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -271,6 +272,7 @@ exports[`src/components/gallery/GalleryCard with an image content should match s
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -457,6 +459,7 @@ exports[`src/components/gallery/GalleryCard with many contents should match snap
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -609,6 +612,7 @@ exports[`src/components/gallery/GalleryCard with many images content should matc
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -927,6 +931,7 @@ exports[`src/components/gallery/GalleryCard without block content should match s
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"

View File

@ -1,37 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {FormattedMessage} from 'react-intl'
import {Board, IPropertyTemplate} from '../../blocks/board'
import {Card} from '../../blocks/card'
import {ContentBlock} from '../../blocks/contentBlock'
import {useSortable} from '../../hooks/sortable'
import mutator from '../../mutator'
import {IUser} from '../../user'
import {getMe} from '../../store/users'
import {getCardContents} from '../../store/contents'
import {useAppSelector} from '../../store/hooks'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
import {Utils} from '../../utils'
import IconButton from '../../widgets/buttons/iconButton'
import DeleteIcon from '../../widgets/icons/delete'
import DuplicateIcon from '../../widgets/icons/duplicate'
import LinkIcon from '../../widgets/icons/Link'
import OptionsIcon from '../../widgets/icons/options'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import Tooltip from '../../widgets/tooltip'
import {Permission} from '../../constants'
import {CardDetailProvider} from '../cardDetail/cardDetailContext'
import ContentElement from '../content/contentElement'
import ImageElement from '../content/imageElement'
import {sendFlashMessage} from '../flashMessages'
import PropertyValueElement from '../propertyValueElement'
import './galleryCard.scss'
import CardBadges from '../cardBadges'
import BoardPermissionGate from '../permissions/boardPermissionGate'
import CardActionsMenu from '../cardActionsMenu/cardActionsMenu'
type Props = {
board: Board
@ -48,10 +38,8 @@ type Props = {
const GalleryCard = (props: Props) => {
const {card, board} = props
const intl = useIntl()
const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort && !props.readonly, props.onDrop)
const contents = useAppSelector(getCardContents(card.id))
const me = useAppSelector<IUser|null>(getMe)
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
@ -84,42 +72,14 @@ const GalleryCard = (props: Props) => {
stopPropagationOnToggle={true}
>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'GalleryCard.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deleteBlock(card, 'delete card')}
/>
<Menu.Text
icon={<DuplicateIcon/>}
id='duplicate'
name={intl.formatMessage({id: 'GalleryCard.duplicate', defaultMessage: 'Duplicate'})}
onClick={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})
mutator.duplicateCard(card.id, board.id)
}}
/>
</BoardPermissionGate>
{me?.id !== 'single-user' &&
<Menu.Text
icon={<LinkIcon/>}
id='copy'
name={intl.formatMessage({id: 'GalleryCard.copyLink', defaultMessage: 'Copy link'})}
onClick={() => {
let cardLink = window.location.href
if (!cardLink.includes(card.id)) {
cardLink += `/${card.id}`
}
Utils.copyTextToClipboard(cardLink)
sendFlashMessage({content: intl.formatMessage({id: 'GalleryCard.copiedLink', defaultMessage: 'Copied!'}), severity: 'high'})
}}
/>
}
</Menu>
<CardActionsMenu
cardId={card!.id}
onClickDelete={() => mutator.deleteBlock(card, 'delete card')}
onClickDuplicate={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})
mutator.duplicateCard(card.id, board.id)
}}
/>
</MenuWrapper>
}

View File

@ -95,6 +95,7 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on copy li
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -248,6 +249,7 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on delete
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"
@ -401,6 +403,7 @@ exports[`src/components/kanban/kanbanCard return kanbanCard and click on duplica
/>
</div>
</div>
<div />
</div>
<div
class="menu-spacer hideOnWidescreen"

View File

@ -11,27 +11,16 @@ import mutator from '../../mutator'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
import {Utils} from '../../utils'
import IconButton from '../../widgets/buttons/iconButton'
import DeleteIcon from '../../widgets/icons/delete'
import DuplicateIcon from '../../widgets/icons/duplicate'
import LinkIcon from '../../widgets/icons/Link'
import OptionsIcon from '../../widgets/icons/options'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import Tooltip from '../../widgets/tooltip'
import {Permission} from '../../constants'
import {sendFlashMessage} from '../flashMessages'
import PropertyValueElement from '../propertyValueElement'
import {IUser} from '../../user'
import {getMe} from '../../store/users'
import {useAppSelector} from '../../store/hooks'
import BoardPermissionGate from '../permissions/boardPermissionGate'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
import './kanbanCard.scss'
import CardBadges from '../cardBadges'
import OpenCardTourStep from '../onboardingTour/openCard/open_card'
import CopyLinkTourStep from '../onboardingTour/copyLink/copy_link'
import CardActionsMenu from '../cardActionsMenu/cardActionsMenu'
export const OnboardingCardClassName = 'onboardingCard'
@ -54,7 +43,6 @@ const KanbanCard = (props: Props) => {
const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly, props.onDrop)
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string}>()
const me = useAppSelector<IUser|null>(getMe)
let className = props.isSelected ? 'KanbanCard selected' : 'KanbanCard'
if (props.isManualSort && isOver) {
className += ' dragover'
@ -116,54 +104,26 @@ const KanbanCard = (props: Props) => {
stopPropagationOnToggle={true}
>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'KanbanCard.delete', defaultMessage: 'Delete'})}
onClick={handleDeleteButtonOnClick}
/>
<Menu.Text
icon={<DuplicateIcon/>}
id='duplicate'
name={intl.formatMessage({id: 'KanbanCard.duplicate', defaultMessage: 'Duplicate'})}
onClick={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})
mutator.duplicateCard(
card.id,
board.id,
false,
'duplicate card',
false,
async (newCardId) => {
props.showCard(newCardId)
},
async () => {
props.showCard(undefined)
},
)
}}
/>
</BoardPermissionGate>
{me?.id !== 'single-user' &&
<Menu.Text
icon={<LinkIcon/>}
id='copy'
name={intl.formatMessage({id: 'KanbanCard.copyLink', defaultMessage: 'Copy link'})}
onClick={() => {
let cardLink = window.location.href
if (!cardLink.includes(card.id)) {
cardLink += `/${card.id}`
}
Utils.copyTextToClipboard(cardLink)
sendFlashMessage({content: intl.formatMessage({id: 'KanbanCard.copiedLink', defaultMessage: 'Copied!'}), severity: 'high'})
}}
/>
}
</Menu>
<CardActionsMenu
cardId={card!.id}
onClickDelete={handleDeleteButtonOnClick}
onClickDuplicate={() => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DuplicateCard, {board: board.id, card: card.id})
mutator.duplicateCard(
card.id,
board.id,
false,
'duplicate card',
false,
async (newCardId) => {
props.showCard(newCardId)
},
async () => {
props.showCard(undefined)
},
)
}}
/>
</MenuWrapper>
}