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