You've already forked focalboard
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:
@ -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');
|
||||
});
|
||||
|
@ -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.",
|
||||
@ -200,4 +204,4 @@
|
||||
"login.register-button": "or create an account if you don't have one",
|
||||
"register.login-button": "or log in if you already have an account",
|
||||
"register.signup-title": "Sign up for your account"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -64,6 +64,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 16px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
> .content {
|
||||
|
@ -50,7 +50,6 @@ const Dialog = React.memo((props: Props) => {
|
||||
className='IconButton--large'
|
||||
/>
|
||||
}
|
||||
<div className='octo-spacer'/>
|
||||
{toolsMenu && <MenuWrapper>
|
||||
<IconButton
|
||||
className='IconButton--large'
|
||||
|
@ -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>
|
||||
`;
|
49
webapp/src/components/sidebar/deleteBoardDialog.scss
Normal file
49
webapp/src/components/sidebar/deleteBoardDialog.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
webapp/src/components/sidebar/deleteBoardDialog.test.tsx
Normal file
57
webapp/src/components/sidebar/deleteBoardDialog.test.tsx
Normal 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>)
|
||||
}
|
||||
})
|
85
webapp/src/components/sidebar/deleteBoardDialog.tsx
Normal file
85
webapp/src/components/sidebar/deleteBoardDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
@ -41,8 +41,4 @@
|
||||
background: rgba(var(--center-channel-color-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.octo-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user