1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-02 14:47:55 +02:00

Deletes newly created card after close if no interaction were made in the card

This commit is contained in:
jav974 2024-10-02 12:19:49 +02:00
parent de5e5cc414
commit b9e0f646ee
10 changed files with 91 additions and 14 deletions

View File

@ -161,9 +161,10 @@ describe('Create and delete board / card', () => {
} }
// Create empty card in last group // Create empty card in last group
cy.log('**Create new empty card in first group**') cy.log('**Create new non empty card in first group**')
cy.get('.octo-board-column').last().contains('+ New').scrollIntoView().click() cy.get('.octo-board-column').last().contains('+ New').scrollIntoView().click()
cy.get('.Dialog').should('exist') cy.get('.Dialog').should('exist')
cy.get('.Dialog .EditableArea.Editable.title').should('exist').type('New card')
cy.get('.Dialog Button[title=\'Close dialog\']').should('be.visible').click() cy.get('.Dialog Button[title=\'Close dialog\']').should('be.visible').click()
cy.get('.KanbanCard').scrollIntoView().should('exist') cy.get('.KanbanCard').scrollIntoView().should('exist')
@ -225,4 +226,23 @@ describe('Create and delete board / card', () => {
type(`{shift+${ctrlKey}+z}`). type(`{shift+${ctrlKey}+z}`).
should('have.text', '') should('have.text', '')
}) })
it('Deletes newly created card after close if no interaction were made in the card', () => {
// Visit a page and create new empty board
cy.visit('/')
cy.wait(500)
cy.uiCreateEmptyBoard()
cy.log('**Create new empty group**')
cy.contains('+ Add a group').scrollIntoView().should('be.visible').click()
cy.get('.KanbanColumnHeader .Editable[value=\'New group\']').should('have.length', 1)
cy.log('**Create new non empty card in first group**')
cy.get('.octo-board-column').last().contains('+ New').scrollIntoView().click()
cy.get('.Dialog').should('exist')
cy.log('**Close dialog without touching any field**')
cy.get('.Dialog Button[title=\'Close dialog\']').should('be.visible').click()
cy.get('.KanbanCard').should('not.exist')
})
}) })

View File

@ -23,7 +23,7 @@ import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../teleme
import BlockIconSelector from '../blockIconSelector' import BlockIconSelector from '../blockIconSelector'
import {useAppDispatch, useAppSelector} from '../../store/hooks' import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {updateCards, setCurrent as setCurrentCard} from '../../store/cards' import {updateCards, setCurrent as setCurrentCard, touchCard} from '../../store/cards'
import {updateContents} from '../../store/contents' import {updateContents} from '../../store/contents'
import {Permission} from '../../constants' import {Permission} from '../../constants'
import {useHasCurrentBoardPermissions} from '../../hooks/permissions' import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
@ -225,7 +225,10 @@ const CardDetail = (props: Props): JSX.Element|null => {
className='title' className='title'
value={title} value={title}
placeholderText='Untitled' placeholderText='Untitled'
onChange={(newTitle: string) => setTitle(newTitle)} onChange={(newTitle: string) => {
setTitle(newTitle)
dispatch(touchCard(card.id))
}}
saveOnEsc={true} saveOnEsc={true}
onSave={saveTitle} onSave={saveTitle}
onCancel={() => setTitle(props.card.title)} onCancel={() => setTitle(props.card.title)}
@ -320,6 +323,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
boardId={card.boardId} boardId={card.boardId}
blocks={blocks} blocks={blocks}
onBlockCreated={async (block: any, afterBlock: any): Promise<BlockData|null> => { onBlockCreated={async (block: any, afterBlock: any): Promise<BlockData|null> => {
dispatch(touchCard(card.id))
if (block.contentType === 'text' && block.value === '') { if (block.contentType === 'text' && block.value === '') {
return null return null
} }
@ -335,6 +339,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
return {...block, id: newBlock.id} return {...block, id: newBlock.id}
}} }}
onBlockModified={async (block: any): Promise<BlockData<any>|null> => { onBlockModified={async (block: any): Promise<BlockData<any>|null> => {
dispatch(touchCard(card.id))
const originalContentBlock = props.contents.flatMap((b) => b).find((b) => b.id === block.id) const originalContentBlock = props.contents.flatMap((b) => b).find((b) => b.id === block.id)
if (!originalContentBlock) { if (!originalContentBlock) {
return null return null
@ -359,6 +364,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
return block return block
}} }}
onBlockMoved={async (block: BlockData, beforeBlock: BlockData|null, afterBlock: BlockData|null): Promise<void> => { onBlockMoved={async (block: BlockData, beforeBlock: BlockData|null, afterBlock: BlockData|null): Promise<void> => {
dispatch(touchCard(card.id))
if (block.id) { if (block.id) {
const idx = card.fields.contentOrder.indexOf(block.id) const idx = card.fields.contentOrder.indexOf(block.id)
let sourceBlockId: string let sourceBlockId: string

View File

@ -16,6 +16,9 @@ import {MarkdownEditor} from '../markdownEditor'
import AddDescriptionTourStep from '../onboardingTour/addDescription/add_description' import AddDescriptionTourStep from '../onboardingTour/addDescription/add_description'
import {touchCard} from '../../store/cards'
import {useAppDispatch} from '../../store/hooks'
import {dragAndDropRearrange} from './cardDetailContentsUtility' import {dragAndDropRearrange} from './cardDetailContentsUtility'
export type Position = 'left' | 'right' | 'above' | 'below' | 'aboveRow' | 'belowRow' export type Position = 'left' | 'right' | 'above' | 'below' | 'aboveRow' | 'belowRow'
@ -161,6 +164,8 @@ const ContentBlockWithDragAndDrop = (props: ContentBlockWithDragAndDropProps) =>
const CardDetailContents = (props: Props) => { const CardDetailContents = (props: Props) => {
const intl = useIntl() const intl = useIntl()
const {contents, card, id} = props const {contents, card, id} = props
const dispatch = useAppDispatch()
if (contents.length) { if (contents.length) {
return ( return (
<div className='octo-content'> <div className='octo-content'>
@ -196,6 +201,7 @@ const CardDetailContents = (props: Props) => {
addTextBlock(card, intl, text) addTextBlock(card, intl, text)
} }
}} }}
onChange={() => dispatch(touchCard(card.id))}
/> />
} }
</div> </div>

View File

@ -3,6 +3,8 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {FormattedMessage, IntlShape, useIntl} from 'react-intl' import {FormattedMessage, IntlShape, useIntl} from 'react-intl'
import {ThunkDispatch} from '@reduxjs/toolkit'
import {BlockTypes} from '../../blocks/block' import {BlockTypes} from '../../blocks/block'
import {Utils} from '../../utils' import {Utils} from '../../utils'
import Button from '../../widgets/buttons/button' import Button from '../../widgets/buttons/button'
@ -11,9 +13,13 @@ import MenuWrapper from '../../widgets/menuWrapper'
import {contentRegistry} from '../content/contentRegistry' import {contentRegistry} from '../content/contentRegistry'
import {touchCard} from '../../store/cards'
import {useAppDispatch} from '../../store/hooks'
import {useCardDetailContext} from './cardDetailContext' import {useCardDetailContext} from './cardDetailContext'
function addContentMenu(intl: IntlShape, type: BlockTypes): JSX.Element { function addContentMenu(intl: IntlShape, type: BlockTypes, dispatch: ThunkDispatch<any, any, any>): JSX.Element {
const handler = contentRegistry.getHandler(type) const handler = contentRegistry.getHandler(type)
if (!handler) { if (!handler) {
Utils.logError(`addContentMenu, unknown content type: ${type}`) Utils.logError(`addContentMenu, unknown content type: ${type}`)
@ -24,6 +30,7 @@ function addContentMenu(intl: IntlShape, type: BlockTypes): JSX.Element {
const {card} = cardDetail const {card} = cardDetail
const index = card.fields.contentOrder.length const index = card.fields.contentOrder.length
cardDetail.addBlock(handler, index, false) cardDetail.addBlock(handler, index, false)
dispatch(touchCard(card.id))
}, [cardDetail, handler]) }, [cardDetail, handler])
return ( return (
@ -39,6 +46,7 @@ function addContentMenu(intl: IntlShape, type: BlockTypes): JSX.Element {
const CardDetailContentsMenu = () => { const CardDetailContentsMenu = () => {
const intl = useIntl() const intl = useIntl()
const dispatch = useAppDispatch()
return ( return (
<div className='CardDetailContentsMenu content add-content'> <div className='CardDetailContentsMenu content add-content'>
<MenuWrapper> <MenuWrapper>
@ -49,7 +57,7 @@ const CardDetailContentsMenu = () => {
/> />
</Button> </Button>
<Menu position='top'> <Menu position='top'>
{contentRegistry.contentTypes.map((type) => addContentMenu(intl, type))} {contentRegistry.contentTypes.map((type) => addContentMenu(intl, type, dispatch))}
</Menu> </Menu>
</MenuWrapper> </MenuWrapper>
</div> </div>

View File

@ -23,6 +23,8 @@ import {Permission} from '../../constants'
import {useHasCurrentBoardPermissions} from '../../hooks/permissions' import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
import propRegistry from '../../properties' import propRegistry from '../../properties'
import {PropertyType} from '../../properties/types' import {PropertyType} from '../../properties/types'
import {touchCard} from '../../store/cards'
import {useAppDispatch} from '../../store/hooks'
type Props = { type Props = {
board: Board board: Board
@ -39,6 +41,7 @@ const CardDetailProperties = (props: Props) => {
const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties]) const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties])
const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards]) const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
const intl = useIntl() const intl = useIntl()
const dispatch = useAppDispatch()
useEffect(() => { useEffect(() => {
const newProperty = board.cardProperties.find((property) => property.id === newTemplateId) const newProperty = board.cardProperties.find((property) => property.id === newTemplateId)
@ -51,6 +54,8 @@ const CardDetailProperties = (props: Props) => {
const [showConfirmationDialog, setShowConfirmationDialog] = useState<boolean>(false) const [showConfirmationDialog, setShowConfirmationDialog] = useState<boolean>(false)
function onPropertyChangeSetAndOpenConfirmationDialog(newType: PropertyType, newName: string, propertyTemplate: IPropertyTemplate) { function onPropertyChangeSetAndOpenConfirmationDialog(newType: PropertyType, newName: string, propertyTemplate: IPropertyTemplate) {
dispatch(touchCard(card.id))
const oldType = propRegistry.get(propertyTemplate.type) const oldType = propRegistry.get(propertyTemplate.type)
// do nothing if no change // do nothing if no change
@ -101,6 +106,8 @@ const CardDetailProperties = (props: Props) => {
} }
function onPropertyDeleteSetAndOpenConfirmationDialog(propertyTemplate: IPropertyTemplate) { function onPropertyDeleteSetAndOpenConfirmationDialog(propertyTemplate: IPropertyTemplate) {
dispatch(touchCard(card.id))
// set ConfirmationDialogBox Props // set ConfirmationDialogBox Props
setConfirmationDialogBox({ setConfirmationDialogBox({
heading: intl.formatMessage({id: 'CardDetailProperty.confirm-delete-heading', defaultMessage: 'Confirm delete property'}), heading: intl.formatMessage({id: 'CardDetailProperty.confirm-delete-heading', defaultMessage: 'Confirm delete property'}),
@ -179,6 +186,7 @@ const CardDetailProperties = (props: Props) => {
<PropertyTypes <PropertyTypes
label={intl.formatMessage({id: 'PropertyMenu.selectType', defaultMessage: 'Select property type'})} label={intl.formatMessage({id: 'PropertyMenu.selectType', defaultMessage: 'Select property type'})}
onTypeSelected={async (type) => { onTypeSelected={async (type) => {
dispatch(touchCard(card.id))
const template: IPropertyTemplate = { const template: IPropertyTemplate = {
id: Utils.createGuid(IDType.BlockID), id: Utils.createGuid(IDType.BlockID),
name: type.displayName(intl), name: type.displayName(intl),

View File

@ -5,7 +5,7 @@ import {FormattedMessage, useIntl} from 'react-intl'
import {CommentBlock, createCommentBlock} from '../../blocks/commentBlock' import {CommentBlock, createCommentBlock} from '../../blocks/commentBlock'
import mutator from '../../mutator' import mutator from '../../mutator'
import {useAppSelector} from '../../store/hooks' import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {Utils} from '../../utils' import {Utils} from '../../utils'
import Button from '../../widgets/buttons/button' import Button from '../../widgets/buttons/button'
@ -18,6 +18,8 @@ import {Permission} from '../../constants'
import AddCommentTourStep from '../onboardingTour/addComments/addComments' import AddCommentTourStep from '../onboardingTour/addComments/addComments'
import {touchCard} from '../../store/cards'
import Comment from './comment' import Comment from './comment'
import './commentsList.scss' import './commentsList.scss'
@ -32,6 +34,8 @@ type Props = {
const CommentsList = (props: Props) => { const CommentsList = (props: Props) => {
const [newComment, setNewComment] = useState('') const [newComment, setNewComment] = useState('')
const me = useAppSelector<IUser|null>(getMe) const me = useAppSelector<IUser|null>(getMe)
const dispatch = useAppDispatch()
const canDeleteOthersComments = useHasCurrentBoardPermissions([Permission.DeleteOthersComments]) const canDeleteOthersComments = useHasCurrentBoardPermissions([Permission.DeleteOthersComments])
const onSendClicked = () => { const onSendClicked = () => {
@ -64,6 +68,8 @@ const CommentsList = (props: Props) => {
text={newComment} text={newComment}
placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})} placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})}
onChange={(value: string) => { onChange={(value: string) => {
dispatch(touchCard(props.cardId))
if (newComment !== value) { if (newComment !== value) {
setNewComment(value) setNewComment(value)
} }

View File

@ -6,11 +6,10 @@ import {FormattedMessage, useIntl} from 'react-intl'
import {Board} from '../blocks/board' import {Board} from '../blocks/board'
import {BoardView} from '../blocks/boardView' import {BoardView} from '../blocks/boardView'
import {Card} from '../blocks/card' import {Card} from '../blocks/card'
import {sendFlashMessage} from '../components/flashMessages'
import mutator from '../mutator' import mutator from '../mutator'
import octoClient from '../octoClient' import octoClient from '../octoClient'
import {getCardAttachments, updateAttachments, updateUploadPrecent} from '../store/attachments' import {getCardAttachments, updateAttachments, updateUploadPrecent} from '../store/attachments'
import {getCard} from '../store/cards' import {getCard, getTouchedCardId, touchCard} from '../store/cards'
import {getCardComments} from '../store/comments' import {getCardComments} from '../store/comments'
import {getCardContents} from '../store/contents' import {getCardContents} from '../store/contents'
import {useAppDispatch, useAppSelector} from '../store/hooks' import {useAppDispatch, useAppSelector} from '../store/hooks'
@ -27,6 +26,8 @@ import {AttachmentBlock, createAttachmentBlock} from '../blocks/attachmentBlock'
import {Block, createBlock} from '../blocks/block' import {Block, createBlock} from '../blocks/block'
import {Permission} from '../constants' import {Permission} from '../constants'
import {sendFlashMessage} from './flashMessages'
import BoardPermissionGate from './permissions/boardPermissionGate' import BoardPermissionGate from './permissions/boardPermissionGate'
import CardDetail from './cardDetail/cardDetail' import CardDetail from './cardDetail/cardDetail'
@ -44,6 +45,7 @@ type Props = {
onClose: () => void onClose: () => void
showCard: (cardId?: string) => void showCard: (cardId?: string) => void
readonly: boolean readonly: boolean
newlyCreated?: boolean
} }
const CardDialog = (props: Props): JSX.Element => { const CardDialog = (props: Props): JSX.Element => {
@ -52,6 +54,7 @@ const CardDialog = (props: Props): JSX.Element => {
const contents = useAppSelector(getCardContents(props.cardId)) const contents = useAppSelector(getCardContents(props.cardId))
const comments = useAppSelector(getCardComments(props.cardId)) const comments = useAppSelector(getCardComments(props.cardId))
const attachments = useAppSelector(getCardAttachments(props.cardId)) const attachments = useAppSelector(getCardAttachments(props.cardId))
const touchedCardId = useAppSelector(getTouchedCardId)
const intl = useIntl() const intl = useIntl()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const isTemplate = card && card.fields.isTemplate const isTemplate = card && card.fields.isTemplate
@ -224,12 +227,22 @@ const CardDialog = (props: Props): JSX.Element => {
) )
} }
const onClose = () => {
dispatch(touchCard(undefined))
if (props.newlyCreated && props.cardId !== touchedCardId) {
handleDeleteCard()
} else {
props.onClose()
}
}
return ( return (
<> <>
<Dialog <Dialog
title={<div/>} title={<div/>}
className='cardDialog' className='cardDialog'
onClose={props.onClose} onClose={onClose}
toolsMenu={!props.readonly && !card?.limited && menu} toolsMenu={!props.readonly && !card?.limited && menu}
toolbar={attachBtn()} toolbar={attachBtn()}
> >
@ -252,7 +265,7 @@ const CardDialog = (props: Props): JSX.Element => {
comments={comments} comments={comments}
attachments={attachments} attachments={attachments}
readonly={props.readonly} readonly={props.readonly}
onClose={props.onClose} onClose={onClose}
onDelete={deleteBlock} onDelete={deleteBlock}
addAttachment={addElement} addAttachment={addElement}
/>} />}

View File

@ -80,6 +80,7 @@ const CenterPanel = (props: Props) => {
const [selectedCardIds, setSelectedCardIds] = useState<string[]>([]) const [selectedCardIds, setSelectedCardIds] = useState<string[]>([])
const [cardIdToFocusOnRender, setCardIdToFocusOnRender] = useState('') const [cardIdToFocusOnRender, setCardIdToFocusOnRender] = useState('')
const [showHiddenCardCountNotification, setShowHiddenCardCountNotification] = useState(false) const [showHiddenCardCountNotification, setShowHiddenCardCountNotification] = useState(false)
const [newlyCreatedCardId, setNewlyCreatedCardId] = useState<string|undefined>(undefined)
const onboardingTourStarted = useAppSelector(getOnboardingTourStarted) const onboardingTourStarted = useAppSelector(getOnboardingTourStarted)
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory) const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
@ -164,7 +165,8 @@ const CenterPanel = (props: Props) => {
} }
}, [selectedCardIds, props.readonly, props.cards]) }, [selectedCardIds, props.readonly, props.cards])
const showCard = useCallback((cardId?: string) => { const showCard = useCallback((cardId?: string, isNew?: boolean) => {
setNewlyCreatedCardId(cardId && isNew ? cardId : undefined)
if (selectedCardIds.length > 0) { if (selectedCardIds.length > 0) {
setSelectedCardIds([]) setSelectedCardIds([])
} }
@ -201,7 +203,7 @@ const CenterPanel = (props: Props) => {
if (show) { if (show) {
dispatch(addCardAction(createCard(block))) dispatch(addCardAction(createCard(block)))
dispatch(updateView({...activeView, fields: {...activeView.fields, cardOrder: [...activeView.fields.cardOrder, block.id]}})) dispatch(updateView({...activeView, fields: {...activeView.fields, cardOrder: [...activeView.fields.cardOrder, block.id]}}))
showCard(block.id) showCard(block.id, true)
} else { } else {
// Focus on this card's title inline on next render // Focus on this card's title inline on next render
setCardIdToFocusOnRender(block.id) setCardIdToFocusOnRender(block.id)
@ -413,6 +415,7 @@ const CenterPanel = (props: Props) => {
onClose={() => showCard(undefined)} onClose={() => showCard(undefined)}
showCard={(cardId) => showCard(cardId)} showCard={(cardId) => showCard(cardId)}
readonly={props.readonly} readonly={props.readonly}
newlyCreated={props.shownCardId === newlyCreatedCardId}
/> />
</RootPortal>} </RootPortal>}

View File

@ -26,7 +26,7 @@ import {UserConfigPatch, UserPreference} from './user'
import store from './store' import store from './store'
import {updateBoards} from './store/boards' import {updateBoards} from './store/boards'
import {updateViews} from './store/views' import {updateViews} from './store/views'
import {updateCards} from './store/cards' import {touchCard, updateCards} from './store/cards'
import {updateAttachments} from './store/attachments' import {updateAttachments} from './store/attachments'
import {updateComments} from './store/comments' import {updateComments} from './store/comments'
import {updateContents} from './store/contents' import {updateContents} from './store/contents'
@ -630,6 +630,8 @@ class Mutator {
} }
async changePropertyValue(boardId: string, card: Card, propertyId: string, value?: string | string[], description = 'change property') { async changePropertyValue(boardId: string, card: Card, propertyId: string, value?: string | string[], description = 'change property') {
store.dispatch(touchCard(card.id))
const oldValue = card.fields.properties[propertyId] const oldValue = card.fields.properties[propertyId]
// dont save anything if property value was not changed. // dont save anything if property value was not changed.

View File

@ -29,6 +29,7 @@ type CardsState = {
cards: {[key: string]: Card} cards: {[key: string]: Card}
templates: {[key: string]: Card} templates: {[key: string]: Card}
cardHiddenWarning: boolean cardHiddenWarning: boolean
touchedCardId?: string
} }
export const refreshCards = createAsyncThunk<Block[], number, {state: RootState}>( export const refreshCards = createAsyncThunk<Block[], number, {state: RootState}>(
@ -106,6 +107,9 @@ const cardsSlice = createSlice({
} }
} }
}, },
touchCard: (state: CardsState, action: PayloadAction<string|undefined>) => {
state.touchedCardId = action.payload
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(refreshCards.fulfilled, (state, action) => { builder.addCase(refreshCards.fulfilled, (state, action) => {
@ -141,7 +145,7 @@ const cardsSlice = createSlice({
}, },
}) })
export const {updateCards, addCard, addTemplate, setCurrent, setLimitTimestamp, showCardHiddenWarning} = cardsSlice.actions export const {updateCards, addCard, addTemplate, setCurrent, setLimitTimestamp, showCardHiddenWarning, touchCard} = cardsSlice.actions
export const {reducer} = cardsSlice export const {reducer} = cardsSlice
export const getCards = (state: RootState): {[key: string]: Card} => state.cards.cards export const getCards = (state: RootState): {[key: string]: Card} => state.cards.cards
@ -410,3 +414,4 @@ export const getCurrentCard = createSelector(
export const getCardLimitTimestamp = (state: RootState): number => state.cards.limitTimestamp export const getCardLimitTimestamp = (state: RootState): number => state.cards.limitTimestamp
export const getCardHiddenWarning = (state: RootState): boolean => state.cards.cardHiddenWarning export const getCardHiddenWarning = (state: RootState): boolean => state.cards.cardHiddenWarning
export const getTouchedCardId = (state: RootState): string|undefined => state.cards.touchedCardId