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:
parent
d87f54d4a4
commit
d2fdf0586c
@ -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',
|
||||
|
@ -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()
|
||||
|
@ -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 })),
|
||||
|
@ -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 */
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user