[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.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",

View file

@ -592,6 +592,415 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
</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`] = `
<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 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)
}

View file

@ -1,5 +1,193 @@
// 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`] = `
<div>
<div
@ -42,6 +230,36 @@ exports[`components/cardDetail/CardDetailProperties should match snapshot 1`] =
</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"
>
@ -106,6 +324,36 @@ exports[`components/cardDetail/CardDetailProperties should show property types m
</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"
>

View file

@ -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((
<CardDetailProperties
board={board!}
card={card}
cards={[card]}
contents={[]}
comments={[]}
activeView={view}
views={views}
readonly={false}
/>
))
return render(component)
}
it('should match snapshot', async () => {
const {container} = render(
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
const {container} = renderComponent()
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', () => {
render(
wrapIntl(
<CardDetailProperties {...cardDetailProps}/>,
),
)
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(
<CardDetailProperties {...cardDetailProps}/>,
),
)
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(
<CardDetailProperties {...cardDetailProps}/>,
),
)
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()
}
})

View file

@ -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<ConfirmationDialogBoxProps>({heading: '', onConfirm: () => {}, onClose: () => {}})
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 (
<div className='octo-propertylist CardDetailProperties'>
@ -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)}
/>
</MenuWrapper>
}
@ -88,21 +169,7 @@ const CardDetailProperties = React.memo((props: Props) => {
{showConfirmationDialog && (
<ConfirmationDialogBox
propertyId={deletingPropId}
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})
}
dialogBox={confirmationDialogBox}
/>
)}

View file

@ -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(
<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 () => {
await act(async () => {
render(wrapDNDIntl(

View file

@ -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<boolean>(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 = (
<Menu position='left'>
@ -66,15 +99,7 @@ const CardDialog = (props: Props): JSX.Element => {
id='delete'
icon={<DeleteIcon/>}
name='Delete'
onClick={async () => {
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()
}}
onClick={handleDeleteButtonOnClick}
/>
<Menu.Text
icon={<LinkIcon/>}
@ -101,11 +126,12 @@ const CardDialog = (props: Props): JSX.Element => {
</Menu>
)
return (
<Dialog
onClose={props.onClose}
toolsMenu={!props.readonly && menu}
>
{card && card.fields.isTemplate &&
<>
<Dialog
onClose={props.onClose}
toolsMenu={!props.readonly && menu}
>
{card && card.fields.isTemplate &&
<div className='banner'>
<FormattedMessage
id='CardDialog.editing-template'
@ -113,7 +139,7 @@ const CardDialog = (props: Props): JSX.Element => {
/>
</div>}
{card &&
{card &&
<CardDetail
board={board}
activeView={activeView}
@ -125,14 +151,17 @@ const CardDialog = (props: Props): JSX.Element => {
readonly={props.readonly}
/>}
{!card &&
{!card &&
<div className='banner error'>
<FormattedMessage
id='CardDialog.nocard'
defaultMessage="This card doesn't exist or is inaccessible."
/>
</div>}
</Dialog>
</Dialog>
{showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
</>
)
}

View file

@ -1,10 +1,11 @@
.confirmation-dialog-box {
.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;
@ -31,30 +32,21 @@
.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;
}

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.
// 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 (
<Dialog
className='confirmation-dialog-box'
onClose={props.onClose}
onClose={handleOnClose}
>
<div className='box-area'>
<h3 className='heading'>{props.heading}</h3>
<p className='sub-text'>{props.subText}</p>
<div
className='box-area'
title='Confirmation Dialog Box'
>
<h3 className='text-heading5'>{props.dialogBox.heading}</h3>
<div className='sub-text'>{props.dialogBox.subText}</div>
<div className='action-buttons'>
<Button
title='Cancel'
active={true}
onClick={props.onClose}
size='medium'
emphasis='tertiary'
onClick={handleOnClose}
>
<FormattedMessage
id='ConfirmationDialog.cancel-action'
@ -39,18 +50,24 @@ export const ConfirmationDialogBox = (props: Props) => {
/>
</Button>
<Button
title='Delete'
title={props.dialogBox.confirmButtonText || 'Confirm'}
size='medium'
submit={true}
emphasis='danger'
onClick={props.onConfirm}
onClick={handleOnConfirm}
>
{ props.dialogBox.confirmButtonText ||
<FormattedMessage
id='ConfirmationDialog.delete-action'
defaultMessage='Delete'
id='ConfirmationDialog.confirm-action'
defaultMessage='Confirm'
/>
}
</Button>
</div>
</div>
</Dialog>
)
}
export default ConfirmationDialogBox
export {ConfirmationDialogBoxProps}

View file

@ -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(
<ReduxProvider store={store}>
<KanbanCard
card={card}
@ -105,6 +105,9 @@ describe('src/components/kanban/kanbanCard', () => {
/>
</ReduxProvider>,
))
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(
<ReduxProvider store={store}>

View file

@ -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<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 (
<div
ref={props.readonly ? () => null : cardRef}
className={className}
draggable={!props.readonly}
style={{opacity: isDragging ? 0.5 : 1}}
onClick={props.onClick}
>
{!props.readonly &&
<>
<div
ref={props.readonly ? () => null : cardRef}
className={className}
draggable={!props.readonly}
style={{opacity: isDragging ? 0.5 : 1}}
onClick={props.onClick}
>
{!props.readonly &&
<MenuWrapper
className='optionsMenu'
stopPropagationOnToggle={true}
@ -68,7 +99,7 @@ const KanbanCard = React.memo((props: Props) => {
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'KanbanCard.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deleteBlock(card, 'delete card')}
onClick={handleDeleteButtonOnClick}
/>
<Menu.Text
icon={<DuplicateIcon/>}
@ -107,29 +138,33 @@ const KanbanCard = React.memo((props: Props) => {
/>
</Menu>
</MenuWrapper>
}
}
<div className='octo-icontitle'>
{ 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 className='octo-icontitle'>
{ 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>
{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>
{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>
{showConfirmationDialogBox && <ConfirmationDialogBox dialogBox={confirmDialogProps}/>}
</>
)
})

View file

@ -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;
}