1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-29 21:01:01 +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.range.label": "Range",
"Calculations.Options.sum.displayName": "Sum", "Calculations.Options.sum.displayName": "Sum",
"Calculations.Options.sum.label": "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-checkboxes": "Checkboxes",
"CardBadges.title-comments": "Comments", "CardBadges.title-comments": "Comments",
"CardBadges.title-description": "This card has a description", "CardBadges.title-description": "This card has a description",
@ -87,8 +92,6 @@
"CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!", "CardDetailProperty.property-deleted": "Deleted {propertyName} successfully!",
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"", "CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
"CardDetial.limited-link": "Learn more about our plans.", "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-button-text": "Delete",
"CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!", "CardDialog.delete-confirmation-dialog-heading": "Confirm card delete!",
"CardDialog.editing-template": "You're editing a template.", "CardDialog.editing-template": "You're editing a template.",
@ -144,17 +147,9 @@
"FindBoardsDialog.NoResultsSubtext": "Check the spelling or try another search.", "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.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", "FindBoardsDialog.Title": "Find Boards",
"GalleryCard.copiedLink": "Copied!",
"GalleryCard.copyLink": "Copy link",
"GalleryCard.delete": "Delete",
"GalleryCard.duplicate": "Duplicate",
"GroupBy.hideEmptyGroups": "Hide {count} empty groups", "GroupBy.hideEmptyGroups": "Hide {count} empty groups",
"GroupBy.showHiddenGroups": "Show {count} hidden groups", "GroupBy.showHiddenGroups": "Show {count} hidden groups",
"GroupBy.ungroup": "Ungroup", "GroupBy.ungroup": "Ungroup",
"KanbanCard.copiedLink": "Copied!",
"KanbanCard.copyLink": "Copy link",
"KanbanCard.delete": "Delete",
"KanbanCard.duplicate": "Duplicate",
"KanbanCard.untitled": "Untitled", "KanbanCard.untitled": "Untitled",
"Mutator.new-board-from-template": "new board from template", "Mutator.new-board-from-template": "new board from template",
"Mutator.new-card-from-template": "new card 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 <div
class="EventContent" 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 <div
class="octo-icontitle" class="octo-icontitle"
> >
@ -2987,6 +3001,20 @@ exports[`components/calendar/toolbar return calendar, with date property not set
<div <div
class="EventContent" 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 <div
class="octo-icontitle" class="octo-icontitle"
> >
@ -5945,6 +5973,20 @@ exports[`components/calendar/toolbar return calendar, with date property set 1`]
<div <div
class="EventContent" 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 <div
class="octo-icontitle" class="octo-icontitle"
> >
@ -7513,6 +7555,20 @@ exports[`components/calendar/toolbar return calendar, without permissions 1`] =
<div <div
class="EventContent" 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 <div
class="octo-icontitle" class="octo-icontitle"
> >

View File

@ -22,6 +22,11 @@ import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
import CardBadges from '../cardBadges' import CardBadges from '../cardBadges'
import './fullcalendar.scss' 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 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 renderEventContent = (eventProps: EventContentArg): JSX.Element|null => {
const {event} = eventProps const {event} = eventProps
const card = cards.find((o) => o.id === event.id) || cards[0]
return ( return (
<div <div
className='EventContent' className='EventContent'
onClick={() => props.showCard(event.id)} 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'> <div className='octo-icontitle'>
{ event.extendedProps.icon ? <div className='octo-icon'>{event.extendedProps.icon}</div> : undefined } { event.extendedProps.icon ? <div className='octo-icon'>{event.extendedProps.icon}</div> : undefined }
<div <div
className='fc-event-title' className='fc-event-title'
key='__title' key='__title'
>{event.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}</div> >{event.title || intl.formatMessage({id: 'CalendarCard.untitled', defaultMessage: 'Untitled'})}</div>
</div> </div>
{visiblePropertyTemplates.map((template) => ( {visiblePropertyTemplates.map((template) => (
<Tooltip <Tooltip
@ -141,14 +162,14 @@ const CalendarFullView = (props: Props): JSX.Element|null => {
<PropertyValueElement <PropertyValueElement
board={board} board={board}
readOnly={true} readOnly={true}
card={cards.find((o) => o.id === event.id) || cards[0]} card={card}
propertyTemplate={template} propertyTemplate={template}
showEmptyPlaceholder={false} showEmptyPlaceholder={false}
/> />
</Tooltip> </Tooltip>
))} ))}
{visibleBadges && {visibleBadges &&
<CardBadges card={cards.find((o) => o.id === event.id) || cards[0]}/> } <CardBadges card={card}/> }
</div> </div>
) )
} }

View File

@ -10,6 +10,10 @@
&:hover { &:hover {
background-color: unset; background-color: unset;
.optionsMenu {
display: block;
}
} }
} }
@ -19,6 +23,20 @@
align-items: flex-start; 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 { .octo-tooltip {
display: flex; display: flex;
max-width: 100%; max-width: 100%;
@ -180,7 +198,6 @@
background-color: rgb(var(--center-channel-bg-rgb)); background-color: rgb(var(--center-channel-bg-rgb));
box-shadow: var(--elevation-1); box-shadow: var(--elevation-1);
margin: 0 8px 10px; margin: 0 8px 10px;
overflow: hidden;
padding: 4px 6px; padding: 4px 6px;
&:hover::before { &: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 TelemetryClient, {TelemetryActions, TelemetryCategory} from '../telemetry/telemetryClient'
import {Utils} from '../utils' import {Utils} from '../utils'
import CompassIcon from '../widgets/icons/compassIcon' import CompassIcon from '../widgets/icons/compassIcon'
import DeleteIcon from '../widgets/icons/delete'
import LinkIcon from '../widgets/icons/Link'
import Menu from '../widgets/menu' import Menu from '../widgets/menu'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../components/confirmationDialogBox' import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../components/confirmationDialogBox'
@ -32,9 +30,9 @@ import BoardPermissionGate from './permissions/boardPermissionGate'
import CardDetail from './cardDetail/cardDetail' import CardDetail from './cardDetail/cardDetail'
import Dialog from './dialog' import Dialog from './dialog'
import {sendFlashMessage} from './flashMessages'
import './cardDialog.scss' import './cardDialog.scss'
import CardActionsMenu from './cardActionsMenu/cardActionsMenu'
type Props = { type Props = {
board: Board board: Board
@ -110,32 +108,10 @@ const CardDialog = (props: Props): JSX.Element => {
} }
const menu = ( const menu = (
<Menu position='left'> <CardActionsMenu
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}> cardId={props.cardId}
<Menu.Text onClickDelete={handleDeleteButtonOnClick}
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'})
}}
/>
}
{!isTemplate && {!isTemplate &&
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}> <BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
<Menu.Text <Menu.Text
@ -149,7 +125,7 @@ const CardDialog = (props: Props): JSX.Element => {
/> />
</BoardPermissionGate> </BoardPermissionGate>
} }
</Menu> </CardActionsMenu>
) )
const followActionButton = (following: boolean): React.ReactNode => { 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 />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"
@ -413,6 +414,7 @@ exports[`src/components/gallery/Gallery should match snapshot without permission
/> />
</div> </div>
</div> </div>
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" 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 />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" 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 />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"
@ -457,6 +459,7 @@ exports[`src/components/gallery/GalleryCard with many contents should match snap
/> />
</div> </div>
</div> </div>
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"
@ -609,6 +612,7 @@ exports[`src/components/gallery/GalleryCard with many images content should matc
/> />
</div> </div>
</div> </div>
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"
@ -927,6 +931,7 @@ exports[`src/components/gallery/GalleryCard without block content should match s
/> />
</div> </div>
</div> </div>
<div />
</div> </div>
<div <div
class="menu-spacer hideOnWidescreen" class="menu-spacer hideOnWidescreen"

View File

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

View File

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

View File

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