diff --git a/src/common/intl/messages.js b/src/common/intl/messages.js index f69fc88b4..fe15db25c 100644 --- a/src/common/intl/messages.js +++ b/src/common/intl/messages.js @@ -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', diff --git a/src/common/modal.js b/src/common/modal.js index 581539163..6b9574811 100644 --- a/src/common/modal.js +++ b/src/common/modal.js @@ -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( - - {body} - , - 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 ( + + {body} +
+
+ {_('enterConfirmText')}{' '} + {_(messageId, values)} +
+
+ +
+
+ ) + } +} + +// ----------------------------------------------------------------------------- + +const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }] + +export const alert = (title, body) => + new Promise(resolve => { + modal( + + {body} + , + resolve + ) + }) + +// ----------------------------------------------------------------------------- + +const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }] + +export const confirm = ({ body, icon = 'alarm', title, strongConfirm }) => + strongConfirm + ? new Promise((resolve, reject) => { + modal( + + ) + }) + : chooseAction({ + body, + buttons: CONFIRM_BUTTONS, + icon, + title, + }) + +// ----------------------------------------------------------------------------- + export default class Modal extends Component { constructor () { super() diff --git a/src/common/xo/index.js b/src/common/xo/index.js index 66d456a8e..14d950ce6 100644 --- a/src/common/xo/index.js +++ b/src/common/xo/index.js @@ -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 })), diff --git a/src/index.scss b/src/index.scss index e41b9c737..47ff4fe4c 100644 --- a/src/index.scss +++ b/src/index.scss @@ -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 */ +}