[GH-1655] Card Delete : added Confirmation Dialog (#1684)

* Made confirmationDialogBox from existing dialog component

* Used ConfirmationDialogBox to raise warning before deletion of card property

* fixes as ci checks did not pass

* fixes to pass ci tests

* Flash Message now visible (changed its z-index)

* Confirmation Dialog shows the property name.

* fixes for eslint test failure

* fixes for eslint test fail

* fixes for eslint test failure

* fix for eslint test failure

* fixed a wrong subtext string

* fixed eslint issues in scss

* i18n en.json for localisation updated

* `en.json;`-wrong file generated by `npm run i18n-extract ` command removed

* On Property Type or Name Change raises warning

* On Property Type or Name Change raises Confirmation dialog

Confirmation dialog box generalized for use

* The affected num of cards calculation added.

* If prop value not filled change after confirmation

* fixes after ci eslint failure

* fixes after ci eslint failure

* In cardDetailProperty test considered dialog box confirmation

* Added test for confirmationDialogBox

* npm run fix and fixed test failure

* snapshot files updated : `npm run updatesnapshot`

* ran i18n-extract script

* Added memo to Confirm dialog component

* reverted the addition of React.memo() as the feature breaks

* added confirmation for card  delete

* default export of Confirmation Dialog Component

* improved cardDialog test considering dialog box opening

* Added memo and useCallback for cnfrm dialog component

* eslint formating

* eslint formatting

* added confirm dialog for kanban and dialog card .

* updated snapshot . cardDetailProperty test failing

* updated snapshot

* Merge branch 'prop-update-warning-1140' into card-delete-warning-1655

* eslint formatting

* Merge branch 'prop-update-warning-1140' into card-delete-warning-1655

* removed unwanted comments

* imported library for failing test

* Updating card modal scss

* Addressed @sbishel comments

* fixed duplicate width in css

* updated comment in kanbanCard

* fixed failing snapshot test

* updated kanbanCard unit test

* npm run fix

* removed useState hook for confirmDialogProps

* removed useState hook from cardDialog and kanbanCard for confirmDialogProps.

* npm run fix

* removed duplicate declaration

Co-authored-by: Prakhar <>
Co-authored-by: prakharporwal <prakharporwal99@gmail.com>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
This commit is contained in:
Prakhar Porwal 2021-11-12 03:33:28 +05:30 committed by GitHub
parent beee6f53e7
commit 27ce296b54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1338 additions and 167 deletions

View file

@ -51,9 +51,16 @@
"CardDetail.addCardText": "add card text", "CardDetail.addCardText": "add card text",
"CardDetail.moveContent": "move card content", "CardDetail.moveContent": "move card content",
"CardDetail.new-comment-placeholder": "Add a comment...", "CardDetail.new-comment-placeholder": "Add a comment...",
"CardDetailProperty.confirm-delete": "Confirm Delete Property", "CardDetailProperty.confirm-delete-heading": "Confirm Delete Property",
"CardDetailProperty.confirm-delete-subtext": "Are you sure you want to delete the property \"{propertyName}\"? Deleting it will delete the property from all cards in this board.", "CardDetailProperty.confirm-delete-subtext": "Are you sure you want to delete the property \"{propertyName}\"? Deleting it will delete the property from all cards in this board.",
"CardDetailProperty.confirm-property-name-change-subtext": "Are you sure you want to change property \"{propertyName}\" {customText}? This will affect value(s) across {numOfCards} card(s) in this board, and can result in data loss.",
"CardDetailProperty.confirm-property-type-change": "Confirm Property Type Change!",
"CardDetailProperty.delete-action-button": "Delete",
"CardDetailProperty.property-change-action-button": "Change Property",
"CardDetailProperty.property-changed": "Changed property successfully!",
"CardDetailProperty.property-deleted": "Deleted {propertyName} Successfully!", "CardDetailProperty.property-deleted": "Deleted {propertyName} Successfully!",
"CardDetailProperty.property-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
"CardDetailProperty.property-type-change-subtext": "name to \"{newPropName}\"",
"CardDialog.copiedLink": "Copied!", "CardDialog.copiedLink": "Copied!",
"CardDialog.copyLink": "Copy link", "CardDialog.copyLink": "Copy link",
"CardDialog.editing-template": "You're editing a template.", "CardDialog.editing-template": "You're editing a template.",
@ -62,6 +69,7 @@
"Comment.delete": "Delete", "Comment.delete": "Delete",
"CommentsList.send": "Send", "CommentsList.send": "Send",
"ConfirmationDialog.cancel-action": "Cancel", "ConfirmationDialog.cancel-action": "Cancel",
"ConfirmationDialog.confirm-action": "Confirm",
"ConfirmationDialog.delete-action": "Delete", "ConfirmationDialog.delete-action": "Delete",
"ContentBlock.Delete": "Delete", "ContentBlock.Delete": "Delete",
"ContentBlock.DeleteAction": "delete", "ContentBlock.DeleteAction": "delete",

View file

@ -592,6 +592,415 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
</div> </div>
`; `;
exports[`components/cardDialog return cardDialog menu content and cancel delete confirmation do nothing 1`] = `
<div>
<div
class="Dialog dialog-back undefined"
>
<div
class="wrapper"
>
<div
class="dialog"
>
<div
class="toolbar"
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="Button IconButton IconButton--large"
type="button"
>
<i
class="CompassIcon icon-dots-horizontal OptionsIcon"
/>
</button>
</div>
</div>
<div
class="CardDetail content"
>
<div
class="BlockIconSelector"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-icon size-l"
>
<span>
i
</span>
</div>
</div>
</div>
<div
class="EditableAreaWrap"
>
<textarea
class="EditableArea Editable title"
height="0"
placeholder="Untitled"
rows="1"
spellcheck="true"
title="title"
>
title
</textarea>
<div
class="EditableAreaContainer"
>
<textarea
aria-hidden="true"
class="EditableAreaReference Editable title"
dir="auto"
disabled=""
rows="1"
>
title
</textarea>
</div>
</div>
<div
class="octo-propertylist CardDetailProperties"
>
<div
class="octo-propertyname add-property"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<span>
+ Add a property
</span>
</button>
</div>
</div>
</div>
<hr />
<div
class="CommentsList"
>
<div
class="commentrow"
>
<img
class="comment-avatar"
src="data:image/svg+xml,<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 100 100\\" style=\\"fill: rgb(192, 192, 192);\\"><rect width=\\"100\\" height=\\"100\\" /></svg>"
/>
<div
class="MarkdownEditor octo-editor newcomment "
>
<div
class="octo-editor-preview octo-placeholder"
/>
<div
class="octo-editor-active Editor"
style="visibility: hidden; position: absolute; top: 0px; left: 0px;"
>
<div
id="test-id-wrapper"
>
<textarea
id="test-id"
style="display: none;"
/>
<div
class="EasyMDEContainer"
>
<div
class="CodeMirror cm-s-easymde CodeMirror-wrap"
>
<div
style="overflow: hidden; position: relative; width: 3px; height: 0px;"
>
<textarea
autocapitalize="off"
autocorrect="off"
spellcheck="false"
style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"
tabindex="0"
/>
</div>
<div
class="CodeMirror-vscrollbar"
cm-not-content="true"
tabindex="-1"
>
<div
style="min-width: 1px;"
/>
</div>
<div
class="CodeMirror-hscrollbar"
cm-not-content="true"
tabindex="-1"
>
<div
style="height: 100%; min-height: 1px;"
/>
</div>
<div
class="CodeMirror-scrollbar-filler"
cm-not-content="true"
/>
<div
class="CodeMirror-gutter-filler"
cm-not-content="true"
/>
<div
class="CodeMirror-scroll"
style="min-height: 10px;"
tabindex="-1"
>
<div
class="CodeMirror-sizer"
style="margin-left: 0px;"
>
<div
style="position: relative;"
>
<div
class="CodeMirror-lines"
role="presentation"
>
<div
role="presentation"
style="position: relative; outline: none;"
>
<div
class="CodeMirror-measure"
>
<pre
class="CodeMirror-line-like"
>
<span>
xxxxxxxxxx
</span>
</pre>
</div>
<div
class="CodeMirror-measure"
/>
<div
style="position: relative; z-index: 1;"
/>
<div
class="CodeMirror-cursors"
/>
<div
class="CodeMirror-code"
role="presentation"
/>
</div>
</div>
</div>
</div>
<div
style="position: absolute; height: 50px; width: 1px;"
/>
<div
class="CodeMirror-gutters"
style="display: none;"
/>
</div>
</div>
<div
class="editor-preview-side editor-preview"
/>
</div>
<div
class="EasyMDEContainer"
>
<div
class="CodeMirror cm-s-easymde CodeMirror-wrap"
>
<div
style="overflow: hidden; position: relative; width: 3px; height: 0px;"
>
<textarea
autocapitalize="off"
autocorrect="off"
spellcheck="false"
style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; outline: none;"
tabindex="0"
/>
</div>
<div
class="CodeMirror-vscrollbar"
cm-not-content="true"
tabindex="-1"
>
<div
style="min-width: 1px;"
/>
</div>
<div
class="CodeMirror-hscrollbar"
cm-not-content="true"
tabindex="-1"
>
<div
style="height: 100%; min-height: 1px;"
/>
</div>
<div
class="CodeMirror-scrollbar-filler"
cm-not-content="true"
/>
<div
class="CodeMirror-gutter-filler"
cm-not-content="true"
/>
<div
class="CodeMirror-scroll"
style="min-height: 10px;"
tabindex="-1"
>
<div
class="CodeMirror-sizer"
style="margin-left: 0px;"
>
<div
style="position: relative;"
>
<div
class="CodeMirror-lines"
role="presentation"
>
<div
role="presentation"
style="position: relative; outline: none;"
>
<div
class="CodeMirror-measure"
>
<pre
class="CodeMirror-line-like"
>
<span>
xxxxxxxxxx
</span>
</pre>
</div>
<div
class="CodeMirror-measure"
/>
<div
style="position: relative; z-index: 1;"
/>
<div
class="CodeMirror-cursors"
/>
<div
class="CodeMirror-code"
role="presentation"
/>
</div>
</div>
</div>
</div>
<div
style="position: absolute; height: 50px; width: 1px;"
/>
<div
class="CodeMirror-gutters"
style="display: none;"
/>
</div>
</div>
<div
class="editor-preview-side editor-preview"
/>
</div>
</div>
</div>
</div>
</div>
<hr />
</div>
</div>
<div
class="CardDetail content fullwidth content-blocks"
>
<div
class="octo-content CardDetailContents"
>
<div
class="octo-block"
>
<div
class="octo-block-margin"
/>
<div
class="MarkdownEditor octo-editor "
>
<div
class="octo-editor-preview octo-placeholder"
/>
<div
class="octo-editor-active Editor"
style="visibility: hidden; position: absolute; top: 0px; left: 0px;"
>
<div
id="test-id-wrapper"
>
<textarea
id="test-id"
style="display: none;"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="CardDetailContentsMenu content add-content"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
type="button"
>
<span>
Add content
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/cardDialog should match snapshot 1`] = ` exports[`components/cardDialog should match snapshot 1`] = `
<div> <div>
<div <div

View file

@ -0,0 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back confirmation-dialog-box"
>
<div
class="wrapper"
>
<div
class="dialog"
>
<div
class="toolbar"
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="box-area"
title="Confirmation Dialog Box"
>
<h3
class="text-heading5"
>
test-heading
</h3>
<div
class="sub-text"
>
test-sub-text
</div>
<div
class="action-buttons"
>
<button
class="Button emphasis--tertiary size--medium"
title="Cancel"
type="button"
>
<span>
Cancel
</span>
</button>
<button
class="Button emphasis--danger size--medium"
title="test-btn-text"
type="submit"
>
<span>
test-btn-text
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Text should match snapshot 1`] = `
<div>
<div
class="Dialog dialog-back confirmation-dialog-box"
>
<div
class="wrapper"
>
<div
class="dialog"
>
<div
class="toolbar"
>
<button
aria-label="Close dialog"
class="Button IconButton IconButton--large"
title="Close dialog"
type="button"
>
<i
class="CompassIcon icon-close CloseIcon"
/>
</button>
</div>
<div
class="box-area"
title="Confirmation Dialog Box"
>
<h3
class="text-heading5"
>
test-heading
</h3>
<div
class="sub-text"
>
test-sub-text
</div>
<div
class="action-buttons"
>
<button
class="Button emphasis--tertiary size--medium"
title="Cancel"
type="button"
>
<span>
Cancel
</span>
</button>
<button
class="Button emphasis--danger size--medium"
title="test-btn-text"
type="submit"
>
<span>
test-btn-text
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -57,6 +57,7 @@ function countEmpty(cards: readonly Card[], property: IPropertyTemplate): string
return String(cards.length - cardsWithValue(cards, property).length) return String(cards.length - cardsWithValue(cards, property).length)
} }
// return count of card which have this property value as not null \\ undefined \\ ''
function countNotEmpty(cards: readonly Card[], property: IPropertyTemplate): string { function countNotEmpty(cards: readonly Card[], property: IPropertyTemplate): string {
return String(cardsWithValue(cards, property).length) return String(cardsWithValue(cards, property).length)
} }

View file

@ -1,5 +1,193 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/cardDetail/CardDetailProperties cancel button in TypeorNameChange dialog should do nothing 1`] = `
<div>
<div
class="octo-propertylist CardDetailProperties"
>
<div
class="octo-propertyrow"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-propertyname"
>
<button
class="Button"
type="button"
>
<span>
Owner
</span>
</button>
</div>
</div>
<div
class="octo-propertyvalue"
data-testid="select-non-editable"
tabindex="0"
>
<span
class="Label propColorDefault "
>
<span
class="Label-text"
>
Jean-Luc Picard
</span>
</span>
</div>
</div>
<div
class="octo-propertyrow"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-propertyname"
>
<button
class="Button"
type="button"
>
<span>
MockStatus
</span>
</button>
</div>
</div>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
spellcheck="false"
style="width: 5px;"
title="1234"
value="1234"
/>
</div>
<div
class="octo-propertyname add-property"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="Button"
type="button"
>
<span>
+ Add a property
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`components/cardDetail/CardDetailProperties cancel on delete dialog should do nothing 1`] = `
<div>
<div
class="octo-propertylist CardDetailProperties"
>
<div
class="octo-propertyrow"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-propertyname"
>
<button
class="Button"
type="button"
>
<span>
Owner
</span>
</button>
</div>
</div>
<div
class="octo-propertyvalue"
data-testid="select-non-editable"
tabindex="0"
>
<span
class="Label propColorDefault "
>
<span
class="Label-text"
>
Jean-Luc Picard
</span>
</span>
</div>
</div>
<div
class="octo-propertyrow"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-propertyname"
>
<button
class="Button"
type="button"
>
<span>
MockStatus
</span>
</button>
</div>
</div>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
spellcheck="false"
style="width: 5px;"
title="1234"
value="1234"
/>
</div>
<div
class="octo-propertyname add-property"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="Button"
type="button"
>
<span>
+ Add a property
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] = ` exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] = `
<div> <div>
<div <div
@ -42,6 +230,36 @@ exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] =
</span> </span>
</div> </div>
</div> </div>
<div
class="octo-propertyrow"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-propertyname"
>
<button
class="Button"
type="button"
>
<span>
MockStatus
</span>
</button>
</div>
</div>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
spellcheck="false"
style="width: 5px;"
title="1234"
value="1234"
/>
</div>
<div <div
class="octo-propertyname add-property" class="octo-propertyname add-property"
> >
@ -106,6 +324,36 @@ exports[`components/cardDetail/CardDetailProperties should show property types m
</span> </span>
</div> </div>
</div> </div>
<div
class="octo-propertyrow"
>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<div
class="octo-propertyname"
>
<button
class="Button"
type="button"
>
<span>
MockStatus
</span>
</button>
</div>
</div>
<input
class="Editable octo-propertyvalue"
placeholder="Empty"
spellcheck="false"
style="width: 5px;"
title="1234"
value="1234"
/>
</div>
<div <div
class="octo-propertyname add-property" class="octo-propertyname add-property"
> >

View file

@ -1,21 +1,19 @@
// 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 from 'react'
import React from 'react'
import {render, screen, act} from '@testing-library/react' import {render, screen, act} from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import {mocked} from 'ts-jest/utils' import {mocked} from 'ts-jest/utils'
import '@testing-library/jest-dom'
import {createIntl} from 'react-intl' import {createIntl} from 'react-intl'
import {PropertyType} from '../../blocks/board'
import {wrapIntl} from '../../testUtils' import {wrapIntl} from '../../testUtils'
import {TestBlockFactory} from '../../test/testBlockFactory' import {TestBlockFactory} from '../../test/testBlockFactory'
import mutator from '../../mutator' import mutator from '../../mutator'
import {propertyTypesList, typeDisplayName} from '../../widgets/propertyMenu' import {propertyTypesList, typeDisplayName} from '../../widgets/propertyMenu'
import {PropertyType} from '../../blocks/board'
import CardDetailProperties from './cardDetailProperties' import CardDetailProperties from './cardDetailProperties'
jest.mock('../../mutator') jest.mock('../../mutator')
@ -46,6 +44,12 @@ describe('components/cardDetail/CardDetailProperties', () => {
}, },
], ],
}, },
{
id: 'property_id_2',
name: 'MockStatus',
type: 'number',
options: [],
},
] ]
const view = TestBlockFactory.createBoardView(board) const view = TestBlockFactory.createBoardView(board)
@ -56,53 +60,37 @@ describe('components/cardDetail/CardDetailProperties', () => {
const card = TestBlockFactory.createCard(board) const card = TestBlockFactory.createCard(board)
card.fields.properties.property_id_1 = 'property_value_id_1' card.fields.properties.property_id_1 = 'property_value_id_1'
card.fields.properties.property_id_2 = '1234'
const cardTemplate = TestBlockFactory.createCard(board)
cardTemplate.fields.isTemplate = true
const cards = [card] const cards = [card]
const cardDetailProps = { function renderComponent() {
board, const component = wrapIntl((
card, <CardDetailProperties
cards, board={board!}
contents: [], card={card}
comments: [], cards={[card]}
activeView: view, contents={[]}
views, comments={[]}
readonly: false, activeView={view}
views={views}
readonly={false}
/>
))
return render(component)
} }
it('should match snapshot', async () => { it('should match snapshot', async () => {
const {container} = render( const {container} = renderComponent()
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
expect(container).toMatchSnapshot() expect(container).toMatchSnapshot()
}) })
it('should rename existing select property', async () => {
render(
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
const menuElement = screen.getByRole('button', {name: 'Owner'})
userEvent.click(menuElement)
const newName = 'Owner - Renamed'
const propertyNameInput = screen.getByRole('textbox')
userEvent.type(propertyNameInput, `${newName}{enter}`)
const propertyTemplate = board.fields.cardProperties[0]
expect(mockedMutator.changePropertyTypeAndName).toHaveBeenCalledTimes(1)
expect(mockedMutator.changePropertyTypeAndName).toHaveBeenCalledWith(board, cards, propertyTemplate, 'select', newName)
})
it('should show confirmation dialog when deleting existing select property', () => { it('should show confirmation dialog when deleting existing select property', () => {
render( renderComponent()
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
const menuElement = screen.getByRole('button', {name: 'Owner'}) const menuElement = screen.getByRole('button', {name: 'Owner'})
userEvent.click(menuElement) userEvent.click(menuElement)
@ -116,11 +104,7 @@ describe('components/cardDetail/CardDetailProperties', () => {
it('should show property types menu', () => { it('should show property types menu', () => {
const intl = createIntl({locale: 'en'}) const intl = createIntl({locale: 'en'})
const {container} = render( const {container} = renderComponent()
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
const menuElement = screen.getByRole('button', {name: /add a property/i}) const menuElement = screen.getByRole('button', {name: /add a property/i})
userEvent.click(menuElement) userEvent.click(menuElement)
@ -135,12 +119,26 @@ describe('components/cardDetail/CardDetailProperties', () => {
}) })
}) })
test('rename select property and confirm button on dialog should rename property', async () => {
const result = renderComponent()
// rename to "Owner-Renamed"
onPropertyRenameOpenConfirmationDialog(result.container)
const propertyTemplate = board.fields.cardProperties[0]
const confirmButton = result.getByTitle('Change Property')
expect(confirmButton).toBeDefined()
userEvent.click(confirmButton!)
// should be called once on confirming renaming the property
expect(mockedMutator.changePropertyTypeAndName).toBeCalledTimes(1)
expect(mockedMutator.changePropertyTypeAndName).toHaveBeenCalledWith(board, cards, propertyTemplate, 'select', 'Owner - Renamed')
})
it('should add new number property', async () => { it('should add new number property', async () => {
render( renderComponent()
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
const menuElement = screen.getByRole('button', {name: /add a property/i}) const menuElement = screen.getByRole('button', {name: /add a property/i})
userEvent.click(menuElement) userEvent.click(menuElement)
@ -151,10 +149,85 @@ describe('components/cardDetail/CardDetailProperties', () => {
}) })
expect(mockedMutator.insertPropertyTemplate).toHaveBeenCalledTimes(1) expect(mockedMutator.insertPropertyTemplate).toHaveBeenCalledTimes(1)
const args = mockedMutator.insertPropertyTemplate.mock.calls[0] const args = mockedMutator.insertPropertyTemplate.mock.calls[0]
const template = args[3] const template = args[3]
expect(template).toBeTruthy() expect(template).toBeTruthy()
expect(template!.name).toMatch(/number/i) expect(template!.name).toMatch(/number/i)
expect(template!.type).toBe('number') expect(template!.type).toBe('number')
}) })
it('cancel button in TypeorNameChange dialog should do nothing', () => {
const result = renderComponent()
const container = result.container
onPropertyRenameOpenConfirmationDialog(container)
const cancelButton = result.getByTitle('Cancel')
expect(cancelButton).toBeDefined()
userEvent.click(cancelButton!)
expect(container).toMatchSnapshot()
})
it('confirmation on delete dialog should delete the property', () => {
const result = renderComponent()
const container = result.container
openDeleteConfirmationDialog(container)
const propertyTemplate = board.fields.cardProperties[0]
const confirmButton = result.getByTitle('Delete')
expect(confirmButton).toBeDefined()
//click delete button
userEvent.click(confirmButton!)
// should be called once on confirming delete
expect(mockedMutator.deleteProperty).toBeCalledTimes(1)
expect(mockedMutator.deleteProperty).toBeCalledWith(board, views, cards, propertyTemplate.id)
})
it('cancel on delete dialog should do nothing', () => {
const result = renderComponent()
const container = result.container
openDeleteConfirmationDialog(container)
const cancelButton = result.getByTitle('Cancel')
expect(cancelButton).toBeDefined()
userEvent.click(cancelButton!)
expect(container).toMatchSnapshot()
})
function openDeleteConfirmationDialog(container:HTMLElement) {
const propertyLabel = container.querySelector('.MenuWrapper')
expect(propertyLabel).toBeDefined()
userEvent.click(propertyLabel!)
const deleteOption = container.querySelector('.MenuOption.TextOption')
expect(propertyLabel).toBeDefined()
userEvent.click(deleteOption!)
const confirmDialog = container.querySelector('.dialog.confirmation-dialog-box')
expect(confirmDialog).toBeDefined()
}
function onPropertyRenameOpenConfirmationDialog(container:HTMLElement) {
const propertyLabel = container.querySelector('.MenuWrapper')
expect(propertyLabel).toBeDefined()
userEvent.click(propertyLabel!)
// write new name in the name text box
const propertyNameInput = container.querySelector('.PropertyMenu.menu-textbox')
expect(propertyNameInput).toBeDefined()
userEvent.type(propertyNameInput!, 'Owner - Renamed{enter}')
userEvent.click(propertyLabel!)
const confirmDialog = container.querySelector('.dialog.confirmation-dialog-box')
expect(confirmDialog).toBeDefined()
}
}) })

View file

@ -8,13 +8,15 @@ import {Card} from '../../blocks/card'
import {BoardView} from '../../blocks/boardView' import {BoardView} from '../../blocks/boardView'
import {ContentBlock} from '../../blocks/contentBlock' import {ContentBlock} from '../../blocks/contentBlock'
import {CommentBlock} from '../../blocks/commentBlock' import {CommentBlock} from '../../blocks/commentBlock'
import mutator from '../../mutator' import mutator from '../../mutator'
import Button from '../../widgets/buttons/button' import Button from '../../widgets/buttons/button'
import MenuWrapper from '../../widgets/menuWrapper' import MenuWrapper from '../../widgets/menuWrapper'
import PropertyMenu, {PropertyTypes, typeDisplayName} from '../../widgets/propertyMenu' import PropertyMenu, {PropertyTypes, typeDisplayName} from '../../widgets/propertyMenu'
import Calculations from '../calculations/calculations'
import PropertyValueElement from '../propertyValueElement' import PropertyValueElement from '../propertyValueElement'
import {ConfirmationDialogBox} from '../confirmationDialogBox' import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
import {sendFlashMessage} from '../flashMessages' import {sendFlashMessage} from '../flashMessages'
import Menu from '../../widgets/menu' import Menu from '../../widgets/menu'
import {IDType, Utils} from '../../utils' import {IDType, Utils} from '../../utils'
@ -42,9 +44,93 @@ const CardDetailProperties = React.memo((props: Props) => {
} }
}, [newTemplateId, board.fields.cardProperties]) }, [newTemplateId, board.fields.cardProperties])
const [confirmationDialogBox, setConfirmationDialogBox] = useState<ConfirmationDialogBoxProps>({heading: '', onConfirm: () => {}, onClose: () => {}})
const [showConfirmationDialog, setShowConfirmationDialog] = useState<boolean>(false) const [showConfirmationDialog, setShowConfirmationDialog] = useState<boolean>(false)
const [deletingPropId, setDeletingPropId] = useState<string>('')
const [deletingPropName, setDeletingPropName] = useState<string>('') function onPropertyChangeSetAndOpenConfirmationDialog(newType: PropertyType, newName: string, propertyTemplate:IPropertyTemplate) {
const oldType = propertyTemplate.type
// do nothing if no change
if (oldType === newType && propertyTemplate.name === newName) {
return
}
const affectsNumOfCards:string = Calculations.countNotEmpty(cards, propertyTemplate, intl)
// if no card has this value set delete the property directly without warning
if (affectsNumOfCards === '0') {
mutator.changePropertyTypeAndName(board, cards, propertyTemplate, newType, newName)
return
}
let subTextString = intl.formatMessage({
id: 'CardDetailProperty.property-name-change-subtext',
defaultMessage: 'type from "{oldPropType}" to "{newPropType}"',
}, {oldPropType: oldType, newPropType: newType})
if (propertyTemplate.name !== newName) {
subTextString = intl.formatMessage({
id: 'CardDetailProperty.property-type-change-subtext',
defaultMessage: 'name to "{newPropName}"',
}, {newPropName: newName})
}
setConfirmationDialogBox({
heading: intl.formatMessage({id: 'CardDetailProperty.confirm-property-type-change', defaultMessage: 'Confirm Property Type Change!'}),
subText: intl.formatMessage({
id: 'CardDetailProperty.confirm-property-name-change-subtext',
defaultMessage: 'Are you sure you want to change property "{propertyName}" {customText}? This will affect value(s) across {numOfCards} card(s) in this board, and can result in data loss.',
},
{
propertyName: propertyTemplate.name,
customText: subTextString,
numOfCards: affectsNumOfCards,
}),
confirmButtonText: intl.formatMessage({id: 'CardDetailProperty.property-change-action-button', defaultMessage: 'Change Property'}),
onConfirm: async () => {
setShowConfirmationDialog(false)
try {
await mutator.changePropertyTypeAndName(board, cards, propertyTemplate, newType, newName)
} catch (err:any) {
Utils.logError(`Error Changing Property And Name:${propertyTemplate.name}: ${err?.toString()}`)
}
sendFlashMessage({content: intl.formatMessage({id: 'CardDetailProperty.property-changed', defaultMessage: 'Changed property successfully!'}), severity: 'high'})
},
onClose: () => setShowConfirmationDialog(false),
})
// open confirmation dialog for property type or name change
setShowConfirmationDialog(true)
}
function onPropertyDeleteSetAndOpenConfirmationDialog(propertyTemplate:IPropertyTemplate) {
// set ConfirmationDialogBox Props
setConfirmationDialogBox({
heading: intl.formatMessage({id: 'CardDetailProperty.confirm-delete-heading', defaultMessage: 'Confirm Delete Property'}),
subText: intl.formatMessage({
id: 'CardDetailProperty.confirm-delete-subtext',
defaultMessage: 'Are you sure you want to delete the property "{propertyName}"? Deleting it will delete the property from all cards in this board.',
},
{propertyName: propertyTemplate.name}),
confirmButtonText: intl.formatMessage({id: 'CardDetailProperty.delete-action-button', defaultMessage: 'Delete'}),
onConfirm: async () => {
const deletingPropName = propertyTemplate.name
setShowConfirmationDialog(false)
try {
await mutator.deleteProperty(board, views, cards, propertyTemplate.id)
sendFlashMessage({content: intl.formatMessage({id: 'CardDetailProperty.property-deleted', defaultMessage: 'Deleted {propertyName} Successfully!'}, {propertyName: deletingPropName}), severity: 'high'})
} catch (err:any) {
Utils.logError(`Error Deleting Property!: Could Not delete Property -" + ${deletingPropName} ${err?.toString()}`)
}
},
onClose: () => setShowConfirmationDialog(false),
})
// open confirmation dialog property delete
setShowConfirmationDialog(true)
}
return ( return (
<div className='octo-propertylist CardDetailProperties'> <div className='octo-propertylist CardDetailProperties'>
@ -63,13 +149,8 @@ const CardDetailProperties = React.memo((props: Props) => {
propertyId={propertyTemplate.id} propertyId={propertyTemplate.id}
propertyName={propertyTemplate.name} propertyName={propertyTemplate.name}
propertyType={propertyTemplate.type} propertyType={propertyTemplate.type}
onTypeAndNameChanged={(newType: PropertyType, newName: string) => mutator.changePropertyTypeAndName(board, cards, propertyTemplate, newType, newName)} onTypeAndNameChanged={(newType: PropertyType, newName: string) => onPropertyChangeSetAndOpenConfirmationDialog(newType, newName, propertyTemplate)}
onDelete={(id: string) => { onDelete={() => onPropertyDeleteSetAndOpenConfirmationDialog(propertyTemplate)}
setDeletingPropId(id)
setDeletingPropName(propertyTemplate.name)
setShowConfirmationDialog(true)
}
}
/> />
</MenuWrapper> </MenuWrapper>
} }
@ -88,21 +169,7 @@ const CardDetailProperties = React.memo((props: Props) => {
{showConfirmationDialog && ( {showConfirmationDialog && (
<ConfirmationDialogBox <ConfirmationDialogBox
propertyId={deletingPropId} dialogBox={confirmationDialogBox}
onClose={() => setShowConfirmationDialog(false)}
onConfirm={() => {
mutator.deleteProperty(board, views, cards, deletingPropId)
setShowConfirmationDialog(false)
sendFlashMessage({content: intl.formatMessage({id: 'CardDetailProperty.property-deleted', defaultMessage: 'Deleted {propertyName} Successfully!'}, {propertyName: deletingPropName}), severity: 'high'})
}}
heading={intl.formatMessage({id: 'CardDetailProperty.confirm-delete', defaultMessage: 'Confirm Delete Property'})}
subText={intl.formatMessage({
id: 'CardDetailProperty.confirm-delete-subtext',
defaultMessage: 'Are you sure you want to delete the property "{propertyName}"? Deleting it will delete the property from all cards in this board.',
},
{propertyName: deletingPropName})
}
/> />
)} )}

View file

@ -159,8 +159,58 @@ describe('components/cardDialog', () => {
userEvent.click(buttonMenu) userEvent.click(buttonMenu)
const buttonDelete = screen.getByRole('button', {name: 'Delete'}) const buttonDelete = screen.getByRole('button', {name: 'Delete'})
userEvent.click(buttonDelete) userEvent.click(buttonDelete)
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
expect(confirmDialog).toBeDefined()
const confirmButton = screen.getByTitle('Delete')
expect(confirmButton).toBeDefined()
//click delete button
userEvent.click(confirmButton!)
// should be called once on confirming delete
expect(mockedMutator.deleteBlock).toBeCalledTimes(1) expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
}) })
test('return cardDialog menu content and cancel delete confirmation do nothing', async () => {
let container
await act(async () => {
const result = render(wrapDNDIntl(
<ReduxProvider store={store}>
<CardDialog
board={board}
activeView={boardView}
views={[boardView]}
cards={[card]}
cardId={card.id}
onClose={jest.fn()}
showCard={jest.fn()}
readonly={false}
/>
</ReduxProvider>,
))
container = result.container
})
const buttonMenu = screen.getAllByRole('button', {name: 'menuwrapper'})[0]
userEvent.click(buttonMenu)
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
userEvent.click(buttonDelete)
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
expect(confirmDialog).toBeDefined()
const cancelButton = screen.getByTitle('Cancel')
expect(cancelButton).toBeDefined()
//click delete button
userEvent.click(cancelButton!)
// should do nothing on cancel delete dialog
expect(container).toMatchSnapshot()
})
test('return cardDialog menu content and do a New template from card', async () => { test('return cardDialog menu content and do a New template from card', async () => {
await act(async () => { await act(async () => {
render(wrapDNDIntl( render(wrapDNDIntl(

View file

@ -1,6 +1,6 @@
// 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 from 'react' import React, {useState} from 'react'
import {FormattedMessage, useIntl} from 'react-intl' import {FormattedMessage, useIntl} from 'react-intl'
import {Board} from '../blocks/board' import {Board} from '../blocks/board'
@ -17,6 +17,8 @@ import DeleteIcon from '../widgets/icons/delete'
import LinkIcon from '../widgets/icons/Link' import LinkIcon from '../widgets/icons/Link'
import Menu from '../widgets/menu' import Menu from '../widgets/menu'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../components/confirmationDialogBox'
import CardDetail from './cardDetail/cardDetail' import CardDetail from './cardDetail/cardDetail'
import Dialog from './dialog' import Dialog from './dialog'
import {sendFlashMessage} from './flashMessages' import {sendFlashMessage} from './flashMessages'
@ -39,6 +41,7 @@ const CardDialog = (props: Props): JSX.Element => {
const comments = useAppSelector(getCardComments(props.cardId)) const comments = useAppSelector(getCardComments(props.cardId))
const intl = useIntl() const intl = useIntl()
const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState<boolean>(false)
const makeTemplateClicked = async () => { const makeTemplateClicked = async () => {
if (!card) { if (!card) {
Utils.assertFailure('card') Utils.assertFailure('card')
@ -59,6 +62,36 @@ const CardDialog = (props: Props): JSX.Element => {
}, },
) )
} }
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 = ( const menu = (
<Menu position='left'> <Menu position='left'>
@ -66,15 +99,7 @@ const CardDialog = (props: Props): JSX.Element => {
id='delete' id='delete'
icon={<DeleteIcon/>} icon={<DeleteIcon/>}
name='Delete' name='Delete'
onClick={async () => { onClick={handleDeleteButtonOnClick}
if (!card) {
Utils.assertFailure()
return
}
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteCard, {board: props.board.id, view: props.activeView.id, card: props.cardId})
await mutator.deleteBlock(card, 'delete card')
props.onClose()
}}
/> />
<Menu.Text <Menu.Text
icon={<LinkIcon/>} icon={<LinkIcon/>}
@ -101,11 +126,12 @@ const CardDialog = (props: Props): JSX.Element => {
</Menu> </Menu>
) )
return ( return (
<Dialog <>
onClose={props.onClose} <Dialog
toolsMenu={!props.readonly && menu} onClose={props.onClose}
> toolsMenu={!props.readonly && menu}
{card && card.fields.isTemplate && >
{card && card.fields.isTemplate &&
<div className='banner'> <div className='banner'>
<FormattedMessage <FormattedMessage
id='CardDialog.editing-template' id='CardDialog.editing-template'
@ -113,7 +139,7 @@ const CardDialog = (props: Props): JSX.Element => {
/> />
</div>} </div>}
{card && {card &&
<CardDetail <CardDetail
board={board} board={board}
activeView={activeView} activeView={activeView}
@ -125,14 +151,17 @@ const CardDialog = (props: Props): JSX.Element => {
readonly={props.readonly} readonly={props.readonly}
/>} />}
{!card && {!card &&
<div className='banner error'> <div className='banner error'>
<FormattedMessage <FormattedMessage
id='CardDialog.nocard' id='CardDialog.nocard'
defaultMessage="This card doesn't exist or is inaccessible." defaultMessage="This card doesn't exist or is inaccessible."
/> />
</div>} </div>}
</Dialog> </Dialog>
{showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
</>
) )
} }

View file

@ -1,10 +1,11 @@
.confirmation-dialog-box { .confirmation-dialog-box {
.dialog { .dialog {
max-width: 512px;
width: 100%;
position: fixed; position: fixed;
top: 30%; top: 30%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: max-content;
height: max-content; height: max-content;
z-index: 300; z-index: 300;
@ -25,36 +26,27 @@
padding: 16px; padding: 16px;
} }
} }
} }
.box-area { .box-area {
display: grid; display: grid;
place-items: center; place-items: center;
padding: 48px 40px;
.heading {
margin-top: 2rem; .text-heading5 {
padding: 2px 4px; margin: 0 0 8px;
} }
.sub-text { .sub-text {
width: 26rem; text-align: center;
word-wrap: normal;
margin: 0.5rem 3rem;
padding: 2px;
@media screen and (max-width: 400px) {
width: 12rem;
}
} }
} }
.action-buttons { .action-buttons {
display: flex; display: grid;
margin: 1rem; grid-gap: 10px;
justify-content: space-between; grid-template-columns: repeat(2, 1fr);
margin-top: 32px;
.Button {
margin: 2px 1rem;
}
} }

View file

@ -0,0 +1,93 @@
// 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 userEvent from '@testing-library/user-event'
import React from 'react'
import {wrapDNDIntl} from '../testUtils'
import ConfirmationDialogBox from './confirmationDialogBox'
describe('/components/confirmationDialogBox', () => {
const dialogPropsWithCnfrmBtnText = {
heading: 'test-heading',
subText: 'test-sub-text',
confirmButtonText: 'test-btn-text',
onConfirm: jest.fn(),
onClose: jest.fn(),
}
const dialogProps = {
heading: 'test-heading',
onConfirm: jest.fn(),
onClose: jest.fn(),
}
it('confirmDialog should match snapshot', async () => {
let container
await act(async () => {
const result = render(
wrapDNDIntl(
<ConfirmationDialogBox
dialogBox={dialogPropsWithCnfrmBtnText}
/>,
),
)
container = result.container
})
expect(container).toMatchSnapshot()
})
it('confirmDialog with Confirm Button Text should match snapshot', async () => {
let containerWithCnfrmBtnText
await act(async () => {
const result = render(
wrapDNDIntl(
<ConfirmationDialogBox
dialogBox={dialogPropsWithCnfrmBtnText}
/>,
),
)
containerWithCnfrmBtnText = result.container
})
expect(containerWithCnfrmBtnText).toMatchSnapshot()
})
it('confirm button click, run onConfirm Function once', () => {
const result = render(
wrapDNDIntl(<ConfirmationDialogBox dialogBox={dialogProps}/>),
)
userEvent.click(result.getByTitle('Confirm'))
expect(dialogProps.onConfirm).toBeCalledTimes(1)
})
it('confirm button (with passed prop text), run onConfirm Function once', () => {
const resultWithConfirmBtnText = render(
wrapDNDIntl(
<ConfirmationDialogBox
dialogBox={dialogPropsWithCnfrmBtnText}
/>,
),
)
userEvent.click(
resultWithConfirmBtnText.getByTitle(dialogPropsWithCnfrmBtnText.confirmButtonText),
)
expect(dialogPropsWithCnfrmBtnText.onConfirm).toBeCalledTimes(1)
})
it('cancel button click runs onClose function', () => {
const result = render(wrapDNDIntl(
<ConfirmationDialogBox
dialogBox={dialogProps}
/>,
))
userEvent.click(result.getByTitle('Cancel'))
expect(dialogProps.onClose).toBeCalledTimes(1)
})
})

View file

@ -1,7 +1,7 @@
// 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 from 'react' import React, {useCallback} from 'react'
import {FormattedMessage} from 'react-intl' import {FormattedMessage} from 'react-intl'
import Button from '../widgets/buttons/button' import Button from '../widgets/buttons/button'
@ -9,29 +9,40 @@ import Button from '../widgets/buttons/button'
import Dialog from './dialog' import Dialog from './dialog'
import './confirmationDialogBox.scss' import './confirmationDialogBox.scss'
type ConfirmationDialogBoxProps = {
heading: string
subText?: string
confirmButtonText?: string
onConfirm: () => void
onClose: () => void
}
type Props = { type Props = {
propertyId: string; dialogBox: ConfirmationDialogBoxProps
onClose: () => void;
onConfirm: () => void;
heading: string;
subText?: string;
} }
export const ConfirmationDialogBox = (props: Props) => { export const ConfirmationDialogBox = (props: Props) => {
const handleOnClose = useCallback(props.dialogBox.onClose, [])
const handleOnConfirm = useCallback(props.dialogBox.onConfirm, [])
return ( return (
<Dialog <Dialog
className='confirmation-dialog-box' className='confirmation-dialog-box'
onClose={props.onClose} onClose={handleOnClose}
> >
<div className='box-area'> <div
<h3 className='heading'>{props.heading}</h3> className='box-area'
<p className='sub-text'>{props.subText}</p> title='Confirmation Dialog Box'
>
<h3 className='text-heading5'>{props.dialogBox.heading}</h3>
<div className='sub-text'>{props.dialogBox.subText}</div>
<div className='action-buttons'> <div className='action-buttons'>
<Button <Button
title='Cancel' title='Cancel'
active={true} size='medium'
onClick={props.onClose} emphasis='tertiary'
onClick={handleOnClose}
> >
<FormattedMessage <FormattedMessage
id='ConfirmationDialog.cancel-action' id='ConfirmationDialog.cancel-action'
@ -39,18 +50,24 @@ export const ConfirmationDialogBox = (props: Props) => {
/> />
</Button> </Button>
<Button <Button
title='Delete' title={props.dialogBox.confirmButtonText || 'Confirm'}
size='medium'
submit={true} submit={true}
emphasis='danger' emphasis='danger'
onClick={props.onConfirm} onClick={handleOnConfirm}
> >
{ props.dialogBox.confirmButtonText ||
<FormattedMessage <FormattedMessage
id='ConfirmationDialog.delete-action' id='ConfirmationDialog.confirm-action'
defaultMessage='Delete' defaultMessage='Confirm'
/> />
}
</Button> </Button>
</div> </div>
</div> </div>
</Dialog> </Dialog>
) )
} }
export default ConfirmationDialogBox
export {ConfirmationDialogBoxProps}

View file

@ -91,7 +91,7 @@ describe('src/components/kanban/kanbanCard', () => {
expect(container).toMatchSnapshot() expect(container).toMatchSnapshot()
}) })
test('return kanbanCard and click on delete menu ', () => { test('return kanbanCard and click on delete menu ', () => {
const {container} = render(wrapDNDIntl( const result = render(wrapDNDIntl(
<ReduxProvider store={store}> <ReduxProvider store={store}>
<KanbanCard <KanbanCard
card={card} card={card}
@ -105,6 +105,9 @@ describe('src/components/kanban/kanbanCard', () => {
/> />
</ReduxProvider>, </ReduxProvider>,
)) ))
const {container} = result
const elementMenuWrapper = screen.getByRole('button', {name: 'menuwrapper'}) const elementMenuWrapper = screen.getByRole('button', {name: 'menuwrapper'})
expect(elementMenuWrapper).not.toBeNull() expect(elementMenuWrapper).not.toBeNull()
userEvent.click(elementMenuWrapper) userEvent.click(elementMenuWrapper)
@ -112,8 +115,16 @@ describe('src/components/kanban/kanbanCard', () => {
const elementButtonDelete = within(elementMenuWrapper).getByRole('button', {name: 'Delete'}) const elementButtonDelete = within(elementMenuWrapper).getByRole('button', {name: 'Delete'})
expect(elementButtonDelete).not.toBeNull() expect(elementButtonDelete).not.toBeNull()
userEvent.click(elementButtonDelete) userEvent.click(elementButtonDelete)
const confirmDialog = screen.getByTitle('Confirmation Dialog Box')
expect(confirmDialog).toBeDefined()
const confirmButton = within(confirmDialog).getByRole('button', {name: 'Delete'})
expect(confirmButton).toBeDefined()
userEvent.click(confirmButton)
expect(mockedMutator.deleteBlock).toBeCalledWith(card, 'delete card') expect(mockedMutator.deleteBlock).toBeCalledWith(card, 'delete card')
}) })
test('return kanbanCard and click on duplicate menu ', () => { test('return kanbanCard and click on duplicate menu ', () => {
const {container} = render(wrapDNDIntl( const {container} = render(wrapDNDIntl(
<ReduxProvider store={store}> <ReduxProvider store={store}>

View file

@ -1,6 +1,6 @@
// 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 from 'react' import React, {useState} from 'react'
import {useIntl} from 'react-intl' import {useIntl} from 'react-intl'
import {Board, IPropertyTemplate} from '../../blocks/board' import {Board, IPropertyTemplate} from '../../blocks/board'
@ -22,6 +22,8 @@ import MenuWrapper from '../../widgets/menuWrapper'
import Tooltip from '../../widgets/tooltip' import Tooltip from '../../widgets/tooltip'
import {sendFlashMessage} from '../flashMessages' import {sendFlashMessage} from '../flashMessages'
import PropertyValueElement from '../propertyValueElement' import PropertyValueElement from '../propertyValueElement'
import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
import './kanbanCard.scss' import './kanbanCard.scss'
type Props = { type Props = {
@ -49,15 +51,44 @@ const KanbanCard = React.memo((props: Props) => {
const contents = useAppSelector(getCardContents(card.id)) const contents = useAppSelector(getCardContents(card.id))
const comments = useAppSelector(getCardComments(card.id)) const comments = useAppSelector(getCardComments(card.id))
const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState<boolean>(false)
const handleDeleteCard = async () => {
if (!card) {
Utils.assertFailure()
return
}
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteCard, {board: board.id, card: card.id})
await mutator.deleteBlock(card, 'delete card')
}
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 = () => {
// 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)
}
return ( return (
<div <>
ref={props.readonly ? () => null : cardRef} <div
className={className} ref={props.readonly ? () => null : cardRef}
draggable={!props.readonly} className={className}
style={{opacity: isDragging ? 0.5 : 1}} draggable={!props.readonly}
onClick={props.onClick} style={{opacity: isDragging ? 0.5 : 1}}
> onClick={props.onClick}
{!props.readonly && >
{!props.readonly &&
<MenuWrapper <MenuWrapper
className='optionsMenu' className='optionsMenu'
stopPropagationOnToggle={true} stopPropagationOnToggle={true}
@ -68,7 +99,7 @@ const KanbanCard = React.memo((props: Props) => {
icon={<DeleteIcon/>} icon={<DeleteIcon/>}
id='delete' id='delete'
name={intl.formatMessage({id: 'KanbanCard.delete', defaultMessage: 'Delete'})} name={intl.formatMessage({id: 'KanbanCard.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deleteBlock(card, 'delete card')} onClick={handleDeleteButtonOnClick}
/> />
<Menu.Text <Menu.Text
icon={<DuplicateIcon/>} icon={<DuplicateIcon/>}
@ -107,29 +138,33 @@ const KanbanCard = React.memo((props: Props) => {
/> />
</Menu> </Menu>
</MenuWrapper> </MenuWrapper>
} }
<div className='octo-icontitle'> <div className='octo-icontitle'>
{ card.fields.icon ? <div className='octo-icon'>{card.fields.icon}</div> : undefined } { card.fields.icon ? <div className='octo-icon'>{card.fields.icon}</div> : undefined }
<div key='__title'>{card.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}</div> <div key='__title'>{card.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}</div>
</div>
{visiblePropertyTemplates.map((template) => (
<Tooltip
key={template.id}
title={template.name}
>
<PropertyValueElement
board={board}
readOnly={true}
card={card}
contents={contents}
comments={comments}
propertyTemplate={template}
showEmptyPlaceholder={false}
/>
</Tooltip>
))}
</div> </div>
{visiblePropertyTemplates.map((template) => (
<Tooltip {showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
key={template.id}
title={template.name} </>
>
<PropertyValueElement
board={board}
readOnly={true}
card={card}
contents={contents}
comments={comments}
propertyTemplate={template}
showEmptyPlaceholder={false}
/>
</Tooltip>
))}
</div>
) )
}) })

View file

@ -74,8 +74,8 @@
} }
&.emphasis--tertiary { &.emphasis--tertiary {
background: rgba(var(--button-bg-rgb), 0.08);
color: rgb(var(--button-bg-rgb)); color: rgb(var(--button-bg-rgb));
background-color: rgb(var(--button-bg-rgb), 0.08);
&:hover { &:hover {
background-color: rgb(var(--button-bg-rgb), 0.12); background-color: rgb(var(--button-bg-rgb), 0.12);
@ -108,6 +108,7 @@
&.size--medium { &.size--medium {
font-size: 14px; font-size: 14px;
font-weight: 600;
padding: 0 20px; padding: 0 20px;
height: 40px; height: 40px;
} }