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',
confirmOk: 'OK',
genericCancel: 'Cancel',
enterConfirmText: 'Enter the following text to confirm:',
// ----- Filters -----
onError: 'On error',
@ -1179,6 +1180,8 @@ const messages = {
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
deleteVmsModalMessage:
'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',
deleteVmModalMessage:
'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 map from 'lodash/map'
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 _ from './intl'
import _, { messages } from './intl'
import Button from './button'
import Icon from './icon'
import propTypes from './prop-types-decorator'
@ -14,8 +16,9 @@ import {
enable as enableShortcuts,
} from './shortcuts'
let instance
// -----------------------------------------------------------------------------
let instance
const modal = (content, onClose) => {
if (!instance) {
throw new Error('No modal instance.')
@ -25,6 +28,19 @@ const modal = (content, onClose) => {
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({
buttons: propTypes.arrayOf(
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 }) => {
return new Promise((resolve, reject) => {
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 {
constructor () {
super()

View File

@ -1052,6 +1052,10 @@ export const deleteVms = vms =>
confirm({
title: _('deleteVmsModalTitle', { vms: vms.length }),
body: _('deleteVmsModalMessage', { vms: vms.length }),
strongConfirm: vms.length > 1 && {
messageId: 'deleteVmsConfirmText',
values: { nVms: vms.length },
},
}).then(
() =>
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 {
font-weight: 700;
}
// =============================================================================
.no-text-selection {
cursor: not-allowed;
-moz-user-select: none; /* Firefox */
user-select: none; /* Chrome */
}