Spaces:
Sleeping
Sleeping
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | |
| // See LICENSE.txt for license information. | |
| import React, {useEffect, useRef, useState, useMemo, useCallback} from 'react' | |
| import {FormattedMessage, useIntl} from 'react-intl' | |
| import {Card} from '../../blocks/card' | |
| import {Board, IPropertyTemplate} from '../../blocks/board' | |
| import {Constants} from '../../constants' | |
| import mutator from '../../mutator' | |
| import Button from '../../widgets/buttons/button' | |
| import Editable from '../../widgets/editable' | |
| import {useSortable} from '../../hooks/sortable' | |
| import {Utils} from '../../utils' | |
| import PropertyValueElement from '../propertyValueElement' | |
| import MenuWrapper from '../../widgets/menuWrapper' | |
| import IconButton from '../../widgets/buttons/iconButton' | |
| import CompassIcon from '../../widgets/icons/compassIcon' | |
| import OptionsIcon from '../../widgets/icons/options' | |
| 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' | |
| import './tableRow.scss' | |
| type Props = { | |
| board: Board | |
| columnWidths: Record<string, number> | |
| isManualSort: boolean | |
| groupById?: string | |
| visiblePropertyIds: string[] | |
| collapsedOptionIds: string[] | |
| card: Card | |
| isSelected: boolean | |
| focusOnMount: boolean | |
| isLastCard: boolean | |
| showCard: (cardId?: string) => void | |
| readonly: boolean | |
| addCard: (groupByOptionId?: string) => Promise<void> | |
| onClick?: (e: React.MouseEvent<HTMLDivElement>, card: Card) => void | |
| onDrop: (srcCard: Card, dstCard: Card) => void | |
| } | |
| const TableRow = (props: Props) => { | |
| const intl = useIntl() | |
| const {board, card, isManualSort, groupById, visiblePropertyIds, collapsedOptionIds} = props | |
| const titleRef = useRef<{ focus(selectAll?: boolean): void }>(null) | |
| const [title, setTitle] = useState(props.card.title || '') | |
| const isGrouped = Boolean(groupById) | |
| const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly && (isManualSort || isGrouped), props.onDrop) | |
| const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState<boolean>(false) | |
| const columnResize = useColumnResize() | |
| useEffect(() => { | |
| if (props.focusOnMount) { | |
| setTimeout(() => titleRef.current?.focus(), 10) | |
| } | |
| }, []) | |
| const onClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => { | |
| props.onClick && props.onClick(e, card) | |
| }, [card, props.onClick]) | |
| const onSaveWithEnter = useCallback(() => { | |
| if (props.isLastCard) { | |
| props.addCard(groupById ? card.fields.properties[groupById!] as string : '') | |
| } | |
| }, [groupById && card.fields.properties[groupById!], props.isLastCard, props.addCard]) | |
| const onSave = useCallback((saveType) => { | |
| if (card.title !== title) { | |
| mutator.changeBlockTitle(props.board.id, card.id, card.title, title) | |
| if (saveType === 'onEnter') { | |
| onSaveWithEnter() | |
| } | |
| } | |
| }, [card.title, title, onSaveWithEnter, board.id, card.id]) | |
| const onTitleChange = useCallback((newTitle: string) => { | |
| setTitle(newTitle) | |
| }, [title, setTitle]) | |
| const visiblePropertyTemplates = useMemo(() => ( | |
| visiblePropertyIds.map((id) => board.cardProperties.find((t) => t.id === id)).filter((i) => i) as IPropertyTemplate[] | |
| ), [board.cardProperties, visiblePropertyIds]) | |
| let className = props.isSelected ? 'TableRow octo-table-row selected' : 'TableRow octo-table-row' | |
| if (isOver) { | |
| className += ' dragover' | |
| } | |
| if (isGrouped) { | |
| const groupID = groupById || '' | |
| let groupValue = card.fields.properties[groupID] as string || 'undefined' | |
| if (groupValue === 'undefined') { | |
| const template = board.cardProperties.find((p) => p.id === groupById) //templates.find((o) => o.id === groupById) | |
| if (template && template.type === 'createdBy') { | |
| groupValue = card.createdBy | |
| } else if (template && template.type === 'updatedBy') { | |
| groupValue = card.modifiedBy | |
| } | |
| } else if (Array.isArray(groupValue)) { | |
| groupValue = groupValue[0] | |
| } | |
| if (collapsedOptionIds.indexOf(groupValue) > -1) { | |
| className += ' hidden' | |
| } | |
| } | |
| if (props.readonly) { | |
| className += ' readonly' | |
| } | |
| const handleDeleteCard = useCallback(async () => { | |
| if (!card) { | |
| Utils.assertFailure() | |
| return | |
| } | |
| TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteCard, {board: board.id, card: card.id}) | |
| await mutator.deleteBlock(card, 'delete card') | |
| }, [card, board.id]) | |
| const confirmDialogProps: ConfirmationDialogBoxProps = useMemo(() => { | |
| return { | |
| 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) | |
| }, | |
| } | |
| }, [handleDeleteCard]) | |
| const handleDeleteButtonOnClick = useCallback(() => { | |
| // user trying to delete a card with blank name | |
| // but content present cannot be deleted without | |
| // confirmation dialog | |
| if (card?.title === '' && card?.fields.contentOrder.length === 0) { | |
| handleDeleteCard() | |
| return | |
| } | |
| setShowConfirmationDialogBox(true) | |
| }, [card.title, card.fields.contentOrder, handleDeleteCard]) | |
| return ( | |
| <div | |
| className={className} | |
| onClick={onClick} | |
| ref={cardRef} | |
| style={{opacity: isDragging ? 0.5 : 1}} | |
| > | |
| <div className='action-cell octo-table-cell-btn'> | |
| {!props.readonly && ( | |
| <IconButton icon={<CompassIcon icon='drag-vertical'/>}/> | |
| )} | |
| </div> | |
| {/* Name / title */} | |
| <div | |
| className='octo-table-cell title-cell' | |
| id='mainBoardHeader' | |
| style={{width: columnResize.width(Constants.titleColumnId)}} | |
| ref={(ref) => columnResize.updateRef(card.id, Constants.titleColumnId, ref)} | |
| > | |
| <div className='octo-icontitle'> | |
| <div className='octo-icon'>{card.fields.icon}</div> | |
| <Editable | |
| ref={titleRef} | |
| value={title} | |
| placeholderText='Untitled' | |
| onChange={onTitleChange} | |
| onSave={onSave} | |
| onCancel={() => setTitle(card.title || '')} | |
| readonly={props.readonly} | |
| spellCheck={true} | |
| /> | |
| </div> | |
| {!props.readonly && ( | |
| <MenuWrapper | |
| className='optionsMenu ml-2 mr-2' | |
| stopPropagationOnToggle={true} | |
| > | |
| <Tooltip | |
| title={intl.formatMessage({id: 'TableRow.MoreOption', defaultMessage: 'More actions'})} | |
| > | |
| <IconButton | |
| title='MenuBtn' | |
| icon={<OptionsIcon/>} | |
| /> | |
| </Tooltip> | |
| <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> | |
| )} | |
| <div className='open-button'> | |
| <Button onClick={() => props.showCard(props.card.id || '')}> | |
| <FormattedMessage | |
| id='TableRow.open' | |
| defaultMessage='Open' | |
| /> | |
| </Button> | |
| </div> | |
| </div> | |
| {/* Columns, one per property */} | |
| {visiblePropertyTemplates.map((template) => { | |
| return ( | |
| <div | |
| className='octo-table-cell' | |
| key={template.id} | |
| style={{width: columnResize.width(template.id)}} | |
| ref={(ref) => columnResize.updateRef(card.id, template.id, ref)} | |
| > | |
| <PropertyValueElement | |
| readOnly={props.readonly} | |
| card={card} | |
| board={board} | |
| propertyTemplate={template} | |
| showEmptyPlaceholder={false} | |
| /> | |
| </div> | |
| ) | |
| })} | |
| {showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>} | |
| </div> | |
| ) | |
| } | |
| export default React.memo(TableRow) | |