feat(modal): strong confirmation modal (#2529)

Fixes #2522 

- New modal that requires the user to type a message to confirm an action
- Integration of this modal on bulk VMs deletion
This commit is contained in:
Pierre Donias 2018-01-25 15:03:16 +01:00 committed by Julien Fontanet
parent d87f54d4a4
commit d2fdf0586c
4 changed files with 142 additions and 35 deletions

View File

@ -22,6 +22,7 @@ const messages = {
alertOk: 'OK', alertOk: 'OK',
confirmOk: 'OK', confirmOk: 'OK',
genericCancel: 'Cancel', genericCancel: 'Cancel',
enterConfirmText: 'Enter the following text to confirm:',
// ----- Filters ----- // ----- Filters -----
onError: 'On error', onError: 'On error',
@ -1179,6 +1180,8 @@ const messages = {
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}', deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
deleteVmsModalMessage: deleteVmsModalMessage:
'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED', 'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
deleteVmsConfirmText:
'delete {nVms, number} vm{nVms, plural, one {} other {s}}',
deleteVmModalTitle: 'Delete VM', deleteVmModalTitle: 'Delete VM',
deleteVmModalMessage: deleteVmModalMessage:
'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED', 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',

View File

@ -2,9 +2,11 @@ import isArray from 'lodash/isArray'
import isString from 'lodash/isString' import isString from 'lodash/isString'
import map from 'lodash/map' import map from 'lodash/map'
import React, { Component, cloneElement } from 'react' import React, { Component, cloneElement } from 'react'
import { createSelector } from 'selectors'
import { injectIntl } from 'react-intl'
import { Modal as ReactModal } from 'react-bootstrap-4/lib' import { Modal as ReactModal } from 'react-bootstrap-4/lib'
import _ from './intl' import _, { messages } from './intl'
import Button from './button' import Button from './button'
import Icon from './icon' import Icon from './icon'
import propTypes from './prop-types-decorator' import propTypes from './prop-types-decorator'
@ -14,8 +16,9 @@ import {
enable as enableShortcuts, enable as enableShortcuts,
} from './shortcuts' } from './shortcuts'
let instance // -----------------------------------------------------------------------------
let instance
const modal = (content, onClose) => { const modal = (content, onClose) => {
if (!instance) { if (!instance) {
throw new Error('No modal instance.') throw new Error('No modal instance.')
@ -25,6 +28,19 @@ const modal = (content, onClose) => {
instance.setState({ content, onClose, showModal: true }, disableShortcuts) instance.setState({ content, onClose, showModal: true }, disableShortcuts)
} }
const _addRef = (component, ref) => {
if (isString(component) || isArray(component)) {
return component
}
try {
return cloneElement(component, { ref })
} catch (_) {} // Stateless component.
return component
}
// -----------------------------------------------------------------------------
@propTypes({ @propTypes({
buttons: propTypes.arrayOf( buttons: propTypes.arrayOf(
propTypes.shape({ propTypes.shape({
@ -105,39 +121,6 @@ class GenericModal extends Component {
} }
} }
const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]
export const alert = (title, body) =>
new Promise(resolve => {
modal(
<GenericModal buttons={ALERT_BUTTONS} resolve={resolve} title={title}>
{body}
</GenericModal>,
resolve
)
})
const _addRef = (component, ref) => {
if (isString(component) || isArray(component)) {
return component
}
try {
return cloneElement(component, { ref })
} catch (_) {} // Stateless component.
return component
}
const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]
export const confirm = ({ body, icon = 'alarm', title }) =>
chooseAction({
body,
buttons: CONFIRM_BUTTONS,
icon,
title,
})
export const chooseAction = ({ body, buttons, icon, title }) => { export const chooseAction = ({ body, buttons, icon, title }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
modal( modal(
@ -155,6 +138,115 @@ export const chooseAction = ({ body, buttons, icon, title }) => {
}) })
} }
@propTypes({
body: propTypes.node,
strongConfirm: propTypes.object.isRequired,
icon: propTypes.string,
reject: propTypes.func,
resolve: propTypes.func,
title: propTypes.node.isRequired,
})
@injectIntl
class StrongConfirm extends Component {
state = {
buttons: [{ btnStyle: 'danger', label: _('confirmOk'), disabled: true }],
}
_getStrongConfirmString = createSelector(
() => this.props.intl.formatMessage,
() => this.props.strongConfirm,
(format, { messageId, values }) => format(messages[messageId], values)
)
_onInputChange = event => {
const userInput = event.target.value
const strongConfirmString = this._getStrongConfirmString()
const confirmButton = this.state.buttons[0]
let disabled
if (
(userInput.toLowerCase() === strongConfirmString.toLowerCase()) ^
(disabled = !confirmButton.disabled)
) {
this.setState({
buttons: [{ ...confirmButton, disabled }],
})
}
}
render () {
const {
body,
strongConfirm: { messageId, values },
icon,
reject,
resolve,
title,
} = this.props
return (
<GenericModal
buttons={this.state.buttons}
icon={icon}
reject={reject}
resolve={resolve}
title={title}
>
{body}
<hr />
<div>
{_('enterConfirmText')}{' '}
<strong className='no-text-selection'>{_(messageId, values)}</strong>
</div>
<div>
<input className='form-control' onChange={this._onInputChange} />
</div>
</GenericModal>
)
}
}
// -----------------------------------------------------------------------------
const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]
export const alert = (title, body) =>
new Promise(resolve => {
modal(
<GenericModal buttons={ALERT_BUTTONS} resolve={resolve} title={title}>
{body}
</GenericModal>,
resolve
)
})
// -----------------------------------------------------------------------------
const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]
export const confirm = ({ body, icon = 'alarm', title, strongConfirm }) =>
strongConfirm
? new Promise((resolve, reject) => {
modal(
<StrongConfirm
body={body}
icon={icon}
reject={reject}
resolve={resolve}
strongConfirm={strongConfirm}
title={title}
/>
)
})
: chooseAction({
body,
buttons: CONFIRM_BUTTONS,
icon,
title,
})
// -----------------------------------------------------------------------------
export default class Modal extends Component { export default class Modal extends Component {
constructor () { constructor () {
super() super()

View File

@ -1052,6 +1052,10 @@ export const deleteVms = vms =>
confirm({ confirm({
title: _('deleteVmsModalTitle', { vms: vms.length }), title: _('deleteVmsModalTitle', { vms: vms.length }),
body: _('deleteVmsModalMessage', { vms: vms.length }), body: _('deleteVmsModalMessage', { vms: vms.length }),
strongConfirm: vms.length > 1 && {
messageId: 'deleteVmsConfirmText',
values: { nVms: vms.length },
},
}).then( }).then(
() => () =>
map(vms, vmId => _call('vm.delete', { id: vmId, delete_disks: true })), map(vms, vmId => _call('vm.delete', { id: vmId, delete_disks: true })),

View File

@ -237,3 +237,11 @@ $select-input-height: 40px; // Bootstrap input height
.notify-title { .notify-title {
font-weight: 700; font-weight: 700;
} }
// =============================================================================
.no-text-selection {
cursor: not-allowed;
-moz-user-select: none; /* Firefox */
user-select: none; /* Chrome */
}