1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-07-15 23:54:29 +02:00

[GH-1278] Confirm delete when deleting board (#1339)

* Delete board confirmation modal (#1278)

* dialog should fill the screen on small resolution

* Updating delete board confirm UI

* Pass onClose callback to Dialog props

* lint

* removing danger-button-bg-rgb from shared variables, adjusting dialog styles to work both in plugin and standalone version of focalboard

* lint

* remove set timeout

* Update button.scss

* update snpashot

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
Co-authored-by: Hossein Ahmadian-Yazdi <hyazdi1997@gmail.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
This commit is contained in:
Grzegorz Tańczyk
2021-11-03 13:57:03 +01:00
committed by GitHub
parent f6c867b4af
commit ef22efd917
13 changed files with 282 additions and 36 deletions

View File

@ -117,6 +117,8 @@ describe('Create and delete board / card', () => {
cy.contains('Delete board').click({force: true});
cy.get('.DeleteBoardDialog button.danger').click({force: true});
// Board should not exist
cy.contains(boardTitle).should('not.exist');
});

View File

@ -47,6 +47,10 @@
"DashboardPage.CenterPanel.NoWorkspacesDescription": "Please try searching for another term",
"DashboardPage.showEmpty": "Show empty",
"DashboardPage.title": "Dashboard",
"DeleteBoardDialog.confirm-cancel": "Cancel",
"DeleteBoardDialog.confirm-delete": "Delete",
"DeleteBoardDialog.confirm-info": "Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete the property from all cards in this board.",
"DeleteBoardDialog.confirm-tite": "Confirm Delete Board",
"Dialog.closeDialog": "Close dialog",
"EditableDayPicker.today": "Today",
"EmptyCenterPanel.no-content": "Add or select a board from the sidebar to get started.",

View File

@ -24,9 +24,6 @@ exports[`components/cardDialog return a cardDialog readonly 1`] = `
class="CompassIcon icon-close CloseIcon"
/>
</button>
<div
class="octo-spacer"
/>
</div>
<div
class="CardDetail content"
@ -123,9 +120,6 @@ exports[`components/cardDialog return cardDialog menu content 1`] = `
class="CompassIcon icon-close CloseIcon"
/>
</button>
<div
class="octo-spacer"
/>
<div
aria-label="menuwrapper"
class="MenuWrapper"
@ -628,9 +622,6 @@ exports[`components/cardDialog should match snapshot 1`] = `
class="CompassIcon icon-close CloseIcon"
/>
</button>
<div
class="octo-spacer"
/>
<div
aria-label="menuwrapper"
class="MenuWrapper"

View File

@ -64,6 +64,7 @@
display: flex;
flex-direction: row;
padding: 16px;
justify-content: space-between;
}
> .content {

View File

@ -50,7 +50,6 @@ const Dialog = React.memo((props: Props) => {
className='IconButton--large'
/>
}
<div className='octo-spacer'/>
{toolsMenu && <MenuWrapper>
<IconButton
className='IconButton--large'

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebar/DeleteBoardDialog Cancel should not submit 1`] = `
<div
id="focalboard-root-portal"
>
exists
</div>
`;
exports[`components/sidebar/DeleteBoardDialog Delete should submit 1`] = `
<div
id="focalboard-root-portal"
>
deleted
</div>
`;

View File

@ -0,0 +1,49 @@
.DeleteBoardDialog {
.dialog {
@media not screen and (max-width: 975px) {
max-width: 512px;
height: max-content;
}
> .toolbar {
padding-bottom: 0;
justify-content: end;
}
}
.container {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 0 40px;
max-width: 100%;
.header {
flex-shrink: 0;
display: flex;
justify-content: center;
margin-top: 0;
margin-bottom: 0;
}
p.body {
display: flex;
flex-grow: 1;
justify-content: center;
text-align: center;
margin: 10px 0;
}
.footer {
display: flex;
flex-direction: row;
flex-shrink: 0;
justify-content: center;
padding: 20px 0 40px;
button:first-child {
margin-right: 10px;
}
}
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import {IntlProvider} from 'react-intl'
import userEvent from '@testing-library/user-event'
import {act, render} from '@testing-library/react'
import DeleteBoardDialog from './deleteBoardDialog'
describe('components/sidebar/DeleteBoardDialog', () => {
it('Cancel should not submit', async () => {
const container = renderTest()
const cancelButton = container.querySelector('.dialog .footer button:not(.danger)')
expect(cancelButton).not.toBeFalsy()
expect(cancelButton?.textContent).toBe('Cancel')
await act(async () => userEvent.click(cancelButton as Element))
expect(container).toMatchSnapshot()
})
it('Delete should submit', async () => {
const container = renderTest()
const deleteButton = container.querySelector('.dialog .footer button.danger')
expect(deleteButton).not.toBeFalsy()
expect(deleteButton?.textContent).toBe('Delete')
await act(async () => userEvent.click(deleteButton as Element))
expect(container).toMatchSnapshot()
})
function renderTest() {
const rootPortalDiv = document.createElement('div')
rootPortalDiv.id = 'focalboard-root-portal'
const {container} = render(<TestComponent/>, {container: document.body.appendChild(rootPortalDiv)})
return container
}
function TestComponent() {
const [isDeleted, setDeleted] = useState(false)
const [isOpen, setOpen] = useState(true)
return (<IntlProvider locale='en'>
{isDeleted ? 'deleted' : 'exists'}
{isOpen &&
<DeleteBoardDialog
boardTitle={'Delete'}
onClose={() => setOpen(false)}
onDelete={async () => setDeleted(true)}
/>}
</IntlProvider>)
}
})

View File

@ -0,0 +1,85 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import {FormattedMessage} from 'react-intl'
import {Utils} from '../../utils'
import Button from '../../widgets/buttons/button'
import Dialog from '../dialog'
import RootPortal from '../rootPortal'
import './deleteBoardDialog.scss'
type Props = {
boardTitle: string;
onClose: () => void;
onDelete: () => Promise<void>
}
export default function DeleteBoardDialog(props: Props): JSX.Element {
const [isSubmitting, setSubmitting] = useState(false)
return (
<RootPortal>
<Dialog
onClose={props.onClose}
toolsMenu={null}
className='DeleteBoardDialog'
>
<div className='container'>
<h2 className='header text-heading5'>
<FormattedMessage
id='DeleteBoardDialog.confirm-tite'
defaultMessage='Confirm Delete Board'
/>
</h2>
<p className='body'>
<FormattedMessage
id='DeleteBoardDialog.confirm-info'
defaultMessage='Are you sure you want to delete the board “{boardTitle}”? Deleting it will delete the property from all cards in this board.'
values={{
boardTitle: props.boardTitle,
}}
/>
</p>
<div className='footer'>
<Button
size={'medium'}
emphasis={'tertiary'}
onClick={() => !isSubmitting && props.onClose()}
>
<FormattedMessage
id='DeleteBoardDialog.confirm-cancel'
defaultMessage='Cancel'
/>
</Button>
<Button
size={'medium'}
filled={true}
danger={true}
onClick={async () => {
try {
setSubmitting(true)
await props.onDelete()
setSubmitting(false)
props.onClose()
} catch (e) {
setSubmitting(false)
Utils.logError(`Delete board ERROR: ${e}`)
// TODO: display error on screen
}
}}
>
<FormattedMessage
id='DeleteBoardDialog.confirm-delete'
defaultMessage='Delete'
/>
</Button>
</div>
</div>
</Dialog>
</RootPortal>
)
}

View File

@ -18,6 +18,9 @@ import OptionsIcon from '../../widgets/icons/options'
import TableIcon from '../../widgets/icons/table'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import DeleteBoardDialog from './deleteBoardDialog'
import './sidebarBoardItem.scss'
type Props = {
@ -33,6 +36,7 @@ const SidebarBoardItem = React.memo((props: Props) => {
const [collapsed, setCollapsed] = useState(false)
const intl = useIntl()
const history = useHistory()
const [deleteBoardOpen, setDeleteBoardOpen] = useState(false)
const match = useRouteMatch<{boardId: string, viewId?: string, cardId?: string, workspaceId?: string}>()
const showBoard = useCallback((boardId) => {
@ -127,23 +131,8 @@ const SidebarBoardItem = React.memo((props: Props) => {
id='deleteBoard'
name={intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'})}
icon={<DeleteIcon/>}
onClick={async () => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteBoard, {board: board.id})
mutator.deleteBlock(
board,
intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'}),
async () => {
if (props.nextBoardId) {
// This delay is needed because WSClient has a default 100 ms notification delay before updates
setTimeout(() => {
showBoard(props.nextBoardId)
}, 120)
}
},
async () => {
showBoard(board.id)
},
)
onClick={() => {
setDeleteBoardOpen(true)
}}
/>
@ -189,6 +178,30 @@ const SidebarBoardItem = React.memo((props: Props) => {
</div>
</div>
))}
{deleteBoardOpen &&
<DeleteBoardDialog
boardTitle={props.board.title}
onClose={() => setDeleteBoardOpen(false)}
onDelete={async () => {
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.DeleteBoard, {board: board.id})
mutator.deleteBlock(
board,
intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'}),
async () => {
if (props.nextBoardId) {
// This delay is needed because WSClient has a default 100 ms notification delay before updates
setTimeout(() => {
showBoard(props.nextBoardId)
}, 120)
}
},
async () => {
showBoard(board.id)
},
)
}}
/>}
</div>
)
})

View File

@ -41,8 +41,4 @@
background: rgba(var(--center-channel-color-rgb), 0.1);
}
}
.octo-spacer {
flex: 1;
}
}

View File

@ -1,4 +1,6 @@
.Button {
--danger-button-bg-rgb: 247, 67, 67;
font-family: 'Open Sans', sans-serif;
display: flex;
flex: 0 0 auto;
@ -15,6 +17,7 @@
color: inherit;
height: 32px;
padding: 0 10px;
font-weight: 600;
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
@ -43,6 +46,18 @@
&:hover {
background-color: rgb(var(--button-bg-rgb), 0.8);
}
&.danger {
background: linear-gradient(rgb(var(--danger-button-bg-rgb)), rgb(var(--danger-button-bg-rgb)));
&:hover {
background: linear-gradient(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.08)), linear-gradient(rgb(var(--danger-button-bg-rgb)), rgb(var(--danger-button-bg-rgb)));
}
&:active {
background: linear-gradient(rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)), linear-gradient(rgb(var(--danger-button-bg-rgb)), rgb(var(--danger-button-bg-rgb)));
}
}
}
&.emphasis--secondary {
@ -58,6 +73,19 @@
}
}
&.emphasis--tertiary {
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);
}
&:active {
background-color: rgb(var(--button-bg-rgb), 0.16);
}
}
&.emphasis--danger {
color: rgb(var(--button-danger-color-rgb));
background-color: rgb(var(--button-danger-bg-rgb));
@ -65,25 +93,27 @@
&:hover {
background-color: rgb(var(--button-danger-bg-rgb), 0.8);
}
}
&.active {
background: rgba(var(--button-bg-rgb), 0.08);
color: rgb(var(--button-bg-rgb));
font-weight: 600;
}
&.size--small {
font-size: 12px;
font-weight: 600;
padding: 0 16px;
height: 32px;
}
&.size--medium {
font-size: 14px;
padding: 0 20px;
height: 40px;
}
&.size--large {
font-size: 16px;
font-weight: 600;
height: 48px;
padding: 0 24px;

View File

@ -16,6 +16,7 @@ type Props = {
submit?: boolean
emphasis?: string
size?: string
danger?: boolean
className?: string
}
@ -24,6 +25,7 @@ function Button(props: Props): JSX.Element {
Button: true,
active: Boolean(props.active),
filled: Boolean(props.filled),
danger: Boolean(props.danger),
}
classNames[`emphasis--${props.emphasis}`] = Boolean(props.emphasis)
classNames[`size--${props.size}`] = Boolean(props.size)