Files
mostlymatter/webapp/boards/src/components/cardDialog.tsx
2023-04-06 13:50:52 -05:00

324 строки
13 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useCallback} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {Board} from 'src/blocks/board'
import {BoardView} from 'src/blocks/boardView'
import {Card} from 'src/blocks/card'
import octoClient from 'src/octoClient'
import mutator from 'src/mutator'
import {getCard} from 'src/store/cards'
import {getCardComments} from 'src/store/comments'
import {getCardContents} from 'src/store/contents'
import {useAppDispatch, useAppSelector} from 'src/store/hooks'
import {getCardAttachments, updateAttachments, updateUploadPrecent} from 'src/store/attachments'
import TelemetryClient, {TelemetryActions, TelemetryCategory} from 'src/telemetry/telemetryClient'
import {Utils} from 'src/utils'
import CompassIcon from 'src/widgets/icons/compassIcon'
import Menu from 'src/widgets/menu'
import {sendFlashMessage} from 'src/components/flashMessages'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from 'src/components/confirmationDialogBox'
import Button from 'src/widgets/buttons/button'
import {getUserBlockSubscriptionList} from 'src/store/initialLoad'
import {getClientConfig} from 'src/store/clientConfig'
import {IUser} from 'src/user'
import {getMe} from 'src/store/users'
import {Permission} from 'src/constants'
import {Block, createBlock} from 'src/blocks/block'
import {AttachmentBlock, createAttachmentBlock} from 'src/blocks/attachmentBlock'
import BoardPermissionGate from './permissions/boardPermissionGate'
import CardDetail from './cardDetail/cardDetail'
import Dialog from './dialog'
import './cardDialog.scss'
import CardActionsMenu from './cardActionsMenu/cardActionsMenu'
type Props = {
board: Board
activeView: BoardView
views: BoardView[]
cards: Card[]
cardId: string
onClose: () => void
showCard: (cardId?: string) => void
readonly: boolean
}
const CardDialog = (props: Props): JSX.Element => {
const {board, activeView, cards, views} = props
const card = useAppSelector(getCard(props.cardId))
const contents = useAppSelector(getCardContents(props.cardId))
const comments = useAppSelector(getCardComments(props.cardId))
const attachments = useAppSelector(getCardAttachments(props.cardId))
const clientConfig = useAppSelector(getClientConfig)
const intl = useIntl()
const dispatch = useAppDispatch()
const me = useAppSelector<IUser|null>(getMe)
const isTemplate = card && card.fields.isTemplate
const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState<boolean>(false)
const makeTemplateClicked = async () => {
if (!card) {
Utils.assertFailure('card')
return
}
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.AddTemplateFromCard, {board: props.board.id, view: activeView.id, card: props.cardId})
await mutator.duplicateCard(
props.cardId,
board.id,
card.fields.isTemplate,
intl.formatMessage({id: 'Mutator.new-template-from-card', defaultMessage: 'new template from card'}),
true,
{},
async (newCardId) => {
props.showCard(newCardId)
},
async () => {
props.showCard(undefined)
},
)
}
const handleDeleteCard = async () => {
if (!card) {
Utils.assertFailure()
return
}
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteCard, {board: props.board.id, view: props.activeView.id, card: card.id})
await mutator.deleteBlock(card, 'delete card')
props.onClose()
}
const confirmDialogProps: ConfirmationDialogBoxProps = {
heading: intl.formatMessage({id: 'CardDialog.delete-confirmation-dialog-heading', defaultMessage: 'Confirm card delete'}),
confirmButtonText: intl.formatMessage({id: 'CardDialog.delete-confirmation-dialog-button-text', defaultMessage: 'Delete'}),
onConfirm: handleDeleteCard,
onClose: () => {
setShowConfirmationDialogBox(false)
},
}
const handleDeleteButtonOnClick = () => {
// use may be renaming a card title
// and accidently delete the card
// so adding des
if (card?.title === '' && card?.fields.contentOrder.length === 0) {
handleDeleteCard()
return
}
setShowConfirmationDialogBox(true)
}
const menu = (
<CardActionsMenu
cardId={props.cardId}
boardId={board.id}
onClickDelete={handleDeleteButtonOnClick}
>
{!isTemplate &&
<BoardPermissionGate permissions={[Permission.ManageBoardProperties]}>
<Menu.Text
id='makeTemplate'
icon={
<CompassIcon
icon='plus'
/>}
name='New template from card'
onClick={makeTemplateClicked}
/>
</BoardPermissionGate>
}
</CardActionsMenu>
)
const removeUploadingAttachment = (uploadingBlock: Block) => {
uploadingBlock.deleteAt = 1
const removeUploadingAttachmentBlock = createAttachmentBlock(uploadingBlock)
dispatch(updateAttachments([removeUploadingAttachmentBlock]))
}
const selectAttachment = (boardId: string) => {
return new Promise<AttachmentBlock>(
(resolve) => {
Utils.selectLocalFile(async (attachment) => {
const uploadingBlock = createBlock()
uploadingBlock.title = attachment.name
uploadingBlock.fields.attachmentId = attachment.name
uploadingBlock.boardId = boardId
if (card) {
uploadingBlock.parentId = card.id
}
const attachmentBlock = createAttachmentBlock(uploadingBlock)
attachmentBlock.isUploading = true
dispatch(updateAttachments([attachmentBlock]))
if (attachment.size > clientConfig.maxFileSize) {
removeUploadingAttachment(uploadingBlock)
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.failed', defaultMessage: "This file couldn't be uploaded as the file size limit has been reached."}), severity: 'normal'})
} else {
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.upload', defaultMessage: 'Attachment uploading.'}), severity: 'normal'})
const xhr = await octoClient.uploadAttachment(boardId, attachment)
if (xhr) {
xhr.upload.onprogress = (event) => {
const percent = Math.floor((event.loaded / event.total) * 100)
dispatch(updateUploadPrecent({
blockId: attachmentBlock.id,
uploadPercent: percent,
}))
}
xhr.onload = () => {
if (xhr.status === 200 && xhr.readyState === 4) {
const json = JSON.parse(xhr.response)
const attachmentId = json.fileId
if (attachmentId) {
removeUploadingAttachment(uploadingBlock)
const block = createAttachmentBlock()
block.fields.attachmentId = attachmentId || ''
block.title = attachment.name
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.uploadSuccess', defaultMessage: 'Attachment uploaded.'}), severity: 'normal'})
resolve(block)
} else {
removeUploadingAttachment(uploadingBlock)
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.failed', defaultMessage: "This file couldn't be uploaded as the file size limit has been reached."}), severity: 'normal'})
}
}
}
}
}
},
'')
},
)
}
const addElement = async () => {
if (!card) {
return
}
const block = await selectAttachment(board.id)
block.parentId = card.id
block.boardId = card.boardId
const typeName = block.type
const description = intl.formatMessage({id: 'AttachmentBlock.addElement', defaultMessage: 'add {type}'}, {type: typeName})
await mutator.insertBlock(block.boardId, block, description)
}
const deleteBlock = useCallback(async (block: Block) => {
if (!card) {
return
}
const description = intl.formatMessage({id: 'AttachmentBlock.DeleteAction', defaultMessage: 'delete'})
await mutator.deleteBlock(block, description)
sendFlashMessage({content: intl.formatMessage({id: 'AttachmentBlock.delete', defaultMessage: 'Attachment deleted.'}), severity: 'normal'})
}, [card?.boardId, card?.id, card?.fields.contentOrder])
const attachBtn = (): React.ReactNode => {
return (
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
<Button
icon={<CompassIcon icon='paperclip'/>}
className='cardFollowBtn cardFollowBtn--attach'
emphasis='gray'
size='medium'
onClick={addElement}
>
{intl.formatMessage({id: 'CardDetail.Attach', defaultMessage: 'Attach'})}
</Button>
</BoardPermissionGate>
)
}
const followActionButton = (following: boolean): React.ReactNode => {
const followBtn = (
<>
<Button
className='cardFollowBtn follow'
emphasis='gray'
size='medium'
onClick={() => mutator.followBlock(props.cardId, 'card', me!.id)}
>
{intl.formatMessage({id: 'CardDetail.Follow', defaultMessage: 'Follow'})}
</Button>
</>
)
const unfollowBtn = (
<>
<Button
className='cardFollowBtn unfollow'
emphasis='tertiary'
size='medium'
onClick={() => mutator.unfollowBlock(props.cardId, 'card', me!.id)}
>
{intl.formatMessage({id: 'CardDetail.Following', defaultMessage: 'Following'})}
</Button>
</>
)
if (!isTemplate && !card?.limited) {
return (<>{attachBtn()}{following ? unfollowBtn : followBtn}</>)
}
return (<>{attachBtn()}</>)
}
const followingCards = useAppSelector(getUserBlockSubscriptionList)
const isFollowingCard = Boolean(followingCards.find((following) => following.blockId === props.cardId))
const toolbar = followActionButton(isFollowingCard)
return (
<>
<Dialog
title={<div/>}
className='cardDialog'
onClose={props.onClose}
toolsMenu={!props.readonly && !card?.limited && menu}
toolbar={toolbar}
>
{isTemplate &&
<div className='banner'>
<FormattedMessage
id='CardDialog.editing-template'
defaultMessage="You're editing a template."
/>
</div>}
{card &&
<CardDetail
board={board}
activeView={activeView}
views={views}
cards={cards}
card={card}
contents={contents}
comments={comments}
attachments={attachments}
readonly={props.readonly}
onClose={props.onClose}
onDelete={deleteBlock}
addAttachment={addElement}
/>}
{!card &&
<div className='banner error'>
<FormattedMessage
id='CardDialog.nocard'
defaultMessage="This card doesn't exist or is inaccessible."
/>
</div>}
</Dialog>
{showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
</>
)
}
export default CardDialog