diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 8411b0e69..72a82c3ae 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -51,9 +51,16 @@
"CardDetail.addCardText": "add card text",
"CardDetail.moveContent": "move card content",
"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-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-name-change-subtext": "type from \"{oldPropType}\" to \"{newPropType}\"",
+ "CardDetailProperty.property-type-change-subtext": "name to \"{newPropName}\"",
"CardDialog.copiedLink": "Copied!",
"CardDialog.copyLink": "Copy link",
"CardDialog.editing-template": "You're editing a template.",
@@ -62,6 +69,7 @@
"Comment.delete": "Delete",
"CommentsList.send": "Send",
"ConfirmationDialog.cancel-action": "Cancel",
+ "ConfirmationDialog.confirm-action": "Confirm",
"ConfirmationDialog.delete-action": "Delete",
"ContentBlock.Delete": "Delete",
"ContentBlock.DeleteAction": "delete",
diff --git a/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap b/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
index 18f72c003..de5404f94 100644
--- a/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
+++ b/webapp/src/components/__snapshots__/cardDialog.test.tsx.snap
@@ -592,6 +592,415 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
`;
+exports[`components/cardDialog return cardDialog menu content and cancel delete confirmation do nothing 1`] = `
+
+
+
+
+
+
+
+
+
+ test-heading
+
+
+ test-sub-text
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Text should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+ test-heading
+
+
+ test-sub-text
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/webapp/src/components/calculations/calculations.ts b/webapp/src/components/calculations/calculations.ts
index 655661d5e..fadfe8aab 100644
--- a/webapp/src/components/calculations/calculations.ts
+++ b/webapp/src/components/calculations/calculations.ts
@@ -57,6 +57,7 @@ function countEmpty(cards: readonly Card[], property: IPropertyTemplate): string
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 {
return String(cardsWithValue(cards, property).length)
}
diff --git a/webapp/src/components/cardDetail/__snapshots__/cardDetailProperties.test.tsx.snap b/webapp/src/components/cardDetail/__snapshots__/cardDetailProperties.test.tsx.snap
index 814a4f13f..80ea0e040 100644
--- a/webapp/src/components/cardDetail/__snapshots__/cardDetailProperties.test.tsx.snap
+++ b/webapp/src/components/cardDetail/__snapshots__/cardDetailProperties.test.tsx.snap
@@ -1,5 +1,193 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`components/cardDetail/CardDetailProperties cancel button in TypeorNameChange dialog should do nothing 1`] = `
+
+
+
+
+
+
+
+ Jean-Luc Picard
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`components/cardDetail/CardDetailProperties cancel on delete dialog should do nothing 1`] = `
+
+
+
+
+
+
+
+ Jean-Luc Picard
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] = `
+
+
+
+
@@ -106,6 +324,36 @@ exports[`components/cardDetail/CardDetailProperties should show property types m
+
diff --git a/webapp/src/components/cardDetail/cardDetailProperties.test.tsx b/webapp/src/components/cardDetail/cardDetailProperties.test.tsx
index 10a481033..f8a5950f3 100644
--- a/webapp/src/components/cardDetail/cardDetailProperties.test.tsx
+++ b/webapp/src/components/cardDetail/cardDetailProperties.test.tsx
@@ -1,21 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react'
+import React from 'react'
import {render, screen, act} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import '@testing-library/jest-dom'
import {mocked} from 'ts-jest/utils'
-
+import '@testing-library/jest-dom'
import {createIntl} from 'react-intl'
+import {PropertyType} from '../../blocks/board'
import {wrapIntl} from '../../testUtils'
import {TestBlockFactory} from '../../test/testBlockFactory'
import mutator from '../../mutator'
import {propertyTypesList, typeDisplayName} from '../../widgets/propertyMenu'
-import {PropertyType} from '../../blocks/board'
-
import CardDetailProperties from './cardDetailProperties'
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)
@@ -56,53 +60,37 @@ describe('components/cardDetail/CardDetailProperties', () => {
const card = TestBlockFactory.createCard(board)
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 cardDetailProps = {
- board,
- card,
- cards,
- contents: [],
- comments: [],
- activeView: view,
- views,
- readonly: false,
+ function renderComponent() {
+ const component = wrapIntl((
+
+ ))
+
+ return render(component)
}
it('should match snapshot', async () => {
- const {container} = render(
- wrapIntl(
-
,
- ),
- )
+ const {container} = renderComponent()
expect(container).toMatchSnapshot()
})
- it('should rename existing select property', async () => {
- render(
- wrapIntl(
-
,
- ),
- )
-
- 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', () => {
- render(
- wrapIntl(
-
,
- ),
- )
+ renderComponent()
const menuElement = screen.getByRole('button', {name: 'Owner'})
userEvent.click(menuElement)
@@ -116,11 +104,7 @@ describe('components/cardDetail/CardDetailProperties', () => {
it('should show property types menu', () => {
const intl = createIntl({locale: 'en'})
- const {container} = render(
- wrapIntl(
-
,
- ),
- )
+ const {container} = renderComponent()
const menuElement = screen.getByRole('button', {name: /add a property/i})
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 () => {
- render(
- wrapIntl(
-
,
- ),
- )
+ renderComponent()
const menuElement = screen.getByRole('button', {name: /add a property/i})
userEvent.click(menuElement)
@@ -151,10 +149,85 @@ describe('components/cardDetail/CardDetailProperties', () => {
})
expect(mockedMutator.insertPropertyTemplate).toHaveBeenCalledTimes(1)
+
const args = mockedMutator.insertPropertyTemplate.mock.calls[0]
const template = args[3]
expect(template).toBeTruthy()
expect(template!.name).toMatch(/number/i)
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()
+ }
})
+
diff --git a/webapp/src/components/cardDetail/cardDetailProperties.tsx b/webapp/src/components/cardDetail/cardDetailProperties.tsx
index 293c4d1a8..b585707f7 100644
--- a/webapp/src/components/cardDetail/cardDetailProperties.tsx
+++ b/webapp/src/components/cardDetail/cardDetailProperties.tsx
@@ -8,13 +8,15 @@ import {Card} from '../../blocks/card'
import {BoardView} from '../../blocks/boardView'
import {ContentBlock} from '../../blocks/contentBlock'
import {CommentBlock} from '../../blocks/commentBlock'
+
import mutator from '../../mutator'
import Button from '../../widgets/buttons/button'
import MenuWrapper from '../../widgets/menuWrapper'
import PropertyMenu, {PropertyTypes, typeDisplayName} from '../../widgets/propertyMenu'
+import Calculations from '../calculations/calculations'
import PropertyValueElement from '../propertyValueElement'
-import {ConfirmationDialogBox} from '../confirmationDialogBox'
+import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
import {sendFlashMessage} from '../flashMessages'
import Menu from '../../widgets/menu'
import {IDType, Utils} from '../../utils'
@@ -42,9 +44,93 @@ const CardDetailProperties = React.memo((props: Props) => {
}
}, [newTemplateId, board.fields.cardProperties])
+ const [confirmationDialogBox, setConfirmationDialogBox] = useState
({heading: '', onConfirm: () => {}, onClose: () => {}})
const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
- const [deletingPropId, setDeletingPropId] = useState('')
- const [deletingPropName, setDeletingPropName] = useState('')
+
+ 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 (
@@ -63,13 +149,8 @@ const CardDetailProperties = React.memo((props: Props) => {
propertyId={propertyTemplate.id}
propertyName={propertyTemplate.name}
propertyType={propertyTemplate.type}
- onTypeAndNameChanged={(newType: PropertyType, newName: string) => mutator.changePropertyTypeAndName(board, cards, propertyTemplate, newType, newName)}
- onDelete={(id: string) => {
- setDeletingPropId(id)
- setDeletingPropName(propertyTemplate.name)
- setShowConfirmationDialog(true)
- }
- }
+ onTypeAndNameChanged={(newType: PropertyType, newName: string) => onPropertyChangeSetAndOpenConfirmationDialog(newType, newName, propertyTemplate)}
+ onDelete={() => onPropertyDeleteSetAndOpenConfirmationDialog(propertyTemplate)}
/>
}
@@ -88,21 +169,7 @@ const CardDetailProperties = React.memo((props: Props) => {
{showConfirmationDialog && (
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})
- }
+ dialogBox={confirmationDialogBox}
/>
)}
diff --git a/webapp/src/components/cardDialog.test.tsx b/webapp/src/components/cardDialog.test.tsx
index 7f9614579..321e2e8e8 100644
--- a/webapp/src/components/cardDialog.test.tsx
+++ b/webapp/src/components/cardDialog.test.tsx
@@ -159,8 +159,58 @@ describe('components/cardDialog', () => {
userEvent.click(buttonMenu)
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
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)
})
+
+ test('return cardDialog menu content and cancel delete confirmation do nothing', async () => {
+ let container
+ await act(async () => {
+ const result = render(wrapDNDIntl(
+
+
+ ,
+ ))
+ 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 () => {
await act(async () => {
render(wrapDNDIntl(
diff --git a/webapp/src/components/cardDialog.tsx b/webapp/src/components/cardDialog.tsx
index 98e9e3c5a..67300478e 100644
--- a/webapp/src/components/cardDialog.tsx
+++ b/webapp/src/components/cardDialog.tsx
@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react'
+import React, {useState} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {Board} from '../blocks/board'
@@ -17,6 +17,8 @@ import DeleteIcon from '../widgets/icons/delete'
import LinkIcon from '../widgets/icons/Link'
import Menu from '../widgets/menu'
+import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../components/confirmationDialogBox'
+
import CardDetail from './cardDetail/cardDetail'
import Dialog from './dialog'
import {sendFlashMessage} from './flashMessages'
@@ -39,6 +41,7 @@ const CardDialog = (props: Props): JSX.Element => {
const comments = useAppSelector(getCardComments(props.cardId))
const intl = useIntl()
+ const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(false)
const makeTemplateClicked = async () => {
if (!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 = (
)
return (
-
+
+ {showConfirmationDialogBox && }
+ >
)
}
diff --git a/webapp/src/components/confirmationDialogBox.scss b/webapp/src/components/confirmationDialogBox.scss
index f9dd34de4..d5b6509a2 100644
--- a/webapp/src/components/confirmationDialogBox.scss
+++ b/webapp/src/components/confirmationDialogBox.scss
@@ -1,10 +1,11 @@
.confirmation-dialog-box {
- .dialog {
+ .dialog {
+ max-width: 512px;
+ width: 100%;
position: fixed;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
- width: max-content;
height: max-content;
z-index: 300;
@@ -25,36 +26,27 @@
padding: 16px;
}
}
-
+
}
.box-area {
display: grid;
place-items: center;
+ padding: 48px 40px;
- .heading {
- margin-top: 2rem;
- padding: 2px 4px;
+
+ .text-heading5 {
+ margin: 0 0 8px;
}
.sub-text {
- width: 26rem;
- word-wrap: normal;
- margin: 0.5rem 3rem;
- padding: 2px;
-
- @media screen and (max-width: 400px) {
- width: 12rem;
- }
+ text-align: center;
}
}
.action-buttons {
- display: flex;
- margin: 1rem;
- justify-content: space-between;
-
- .Button {
- margin: 2px 1rem;
- }
+ display: grid;
+ grid-gap: 10px;
+ grid-template-columns: repeat(2, 1fr);
+ margin-top: 32px;
}
diff --git a/webapp/src/components/confirmationDialogBox.test.tsx b/webapp/src/components/confirmationDialogBox.test.tsx
new file mode 100644
index 000000000..e83f57afb
--- /dev/null
+++ b/webapp/src/components/confirmationDialogBox.test.tsx
@@ -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(
+ ,
+ ),
+ )
+ 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(
+ ,
+ ),
+ )
+ containerWithCnfrmBtnText = result.container
+ })
+ expect(containerWithCnfrmBtnText).toMatchSnapshot()
+ })
+
+ it('confirm button click, run onConfirm Function once', () => {
+ const result = render(
+ wrapDNDIntl(),
+ )
+
+ 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(
+ ,
+ ),
+ )
+
+ userEvent.click(
+ resultWithConfirmBtnText.getByTitle(dialogPropsWithCnfrmBtnText.confirmButtonText),
+ )
+
+ expect(dialogPropsWithCnfrmBtnText.onConfirm).toBeCalledTimes(1)
+ })
+
+ it('cancel button click runs onClose function', () => {
+ const result = render(wrapDNDIntl(
+ ,
+ ))
+
+ userEvent.click(result.getByTitle('Cancel'))
+ expect(dialogProps.onClose).toBeCalledTimes(1)
+ })
+})
diff --git a/webapp/src/components/confirmationDialogBox.tsx b/webapp/src/components/confirmationDialogBox.tsx
index 1632a6a2b..204827fd3 100644
--- a/webapp/src/components/confirmationDialogBox.tsx
+++ b/webapp/src/components/confirmationDialogBox.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react'
+import React, {useCallback} from 'react'
import {FormattedMessage} from 'react-intl'
import Button from '../widgets/buttons/button'
@@ -9,29 +9,40 @@ import Button from '../widgets/buttons/button'
import Dialog from './dialog'
import './confirmationDialogBox.scss'
+type ConfirmationDialogBoxProps = {
+ heading: string
+ subText?: string
+ confirmButtonText?: string
+ onConfirm: () => void
+ onClose: () => void
+}
+
type Props = {
- propertyId: string;
- onClose: () => void;
- onConfirm: () => void;
- heading: string;
- subText?: string;
+ dialogBox: ConfirmationDialogBoxProps
}
export const ConfirmationDialogBox = (props: Props) => {
+ const handleOnClose = useCallback(props.dialogBox.onClose, [])
+ const handleOnConfirm = useCallback(props.dialogBox.onConfirm, [])
+
return (
-
-
{props.heading}
-
{props.subText}
+
+
{props.dialogBox.heading}
+
{props.dialogBox.subText}
)
}
+
+export default ConfirmationDialogBox
+export {ConfirmationDialogBoxProps}
diff --git a/webapp/src/components/kanban/kanbanCard.test.tsx b/webapp/src/components/kanban/kanbanCard.test.tsx
index 340ba7171..fb9675443 100644
--- a/webapp/src/components/kanban/kanbanCard.test.tsx
+++ b/webapp/src/components/kanban/kanbanCard.test.tsx
@@ -91,7 +91,7 @@ describe('src/components/kanban/kanbanCard', () => {
expect(container).toMatchSnapshot()
})
test('return kanbanCard and click on delete menu ', () => {
- const {container} = render(wrapDNDIntl(
+ const result = render(wrapDNDIntl(
{
/>
,
))
+
+ const {container} = result
+
const elementMenuWrapper = screen.getByRole('button', {name: 'menuwrapper'})
expect(elementMenuWrapper).not.toBeNull()
userEvent.click(elementMenuWrapper)
@@ -112,8 +115,16 @@ describe('src/components/kanban/kanbanCard', () => {
const elementButtonDelete = within(elementMenuWrapper).getByRole('button', {name: 'Delete'})
expect(elementButtonDelete).not.toBeNull()
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')
})
+
test('return kanbanCard and click on duplicate menu ', () => {
const {container} = render(wrapDNDIntl(
diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx
index 3647dca9a..8c8e8dab3 100644
--- a/webapp/src/components/kanban/kanbanCard.tsx
+++ b/webapp/src/components/kanban/kanbanCard.tsx
@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react'
+import React, {useState} from 'react'
import {useIntl} from 'react-intl'
import {Board, IPropertyTemplate} from '../../blocks/board'
@@ -22,6 +22,8 @@ import MenuWrapper from '../../widgets/menuWrapper'
import Tooltip from '../../widgets/tooltip'
import {sendFlashMessage} from '../flashMessages'
import PropertyValueElement from '../propertyValueElement'
+
+import ConfirmationDialogBox, {ConfirmationDialogBoxProps} from '../confirmationDialogBox'
import './kanbanCard.scss'
type Props = {
@@ -49,15 +51,44 @@ const KanbanCard = React.memo((props: Props) => {
const contents = useAppSelector(getCardContents(card.id))
const comments = useAppSelector(getCardComments(card.id))
+ const [showConfirmationDialogBox, setShowConfirmationDialogBox] = useState(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 (
- null : cardRef}
- className={className}
- draggable={!props.readonly}
- style={{opacity: isDragging ? 0.5 : 1}}
- onClick={props.onClick}
- >
- {!props.readonly &&
+ <>
+
null : cardRef}
+ className={className}
+ draggable={!props.readonly}
+ style={{opacity: isDragging ? 0.5 : 1}}
+ onClick={props.onClick}
+ >
+ {!props.readonly &&
{
icon={}
id='delete'
name={intl.formatMessage({id: 'KanbanCard.delete', defaultMessage: 'Delete'})}
- onClick={() => mutator.deleteBlock(card, 'delete card')}
+ onClick={handleDeleteButtonOnClick}
/>
}
@@ -107,29 +138,33 @@ const KanbanCard = React.memo((props: Props) => {
/>
- }
+ }
-
- { card.fields.icon ?
{card.fields.icon}
: undefined }
-
{card.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}
+
+ { card.fields.icon ?
{card.fields.icon}
: undefined }
+
{card.title || intl.formatMessage({id: 'KanbanCard.untitled', defaultMessage: 'Untitled'})}
+
+ {visiblePropertyTemplates.map((template) => (
+
+
+
+ ))}
- {visiblePropertyTemplates.map((template) => (
-
-
-
- ))}
-
+
+ {showConfirmationDialogBox &&
}
+
+ >
)
})
diff --git a/webapp/src/widgets/buttons/button.scss b/webapp/src/widgets/buttons/button.scss
index 91fb1435e..6c4f7e578 100644
--- a/webapp/src/widgets/buttons/button.scss
+++ b/webapp/src/widgets/buttons/button.scss
@@ -74,8 +74,8 @@
}
&.emphasis--tertiary {
+ background: rgba(var(--button-bg-rgb), 0.08);
color: rgb(var(--button-bg-rgb));
- background-color: rgb(var(--button-bg-rgb), 0.08);
&:hover {
background-color: rgb(var(--button-bg-rgb), 0.12);
@@ -108,6 +108,7 @@
&.size--medium {
font-size: 14px;
+ font-weight: 600;
padding: 0 20px;
height: 40px;
}
+ + xxxxxxxxxx + +
++ + xxxxxxxxxx + +
++