feat(xo-web/editable): Select supports multiple values (#3623)
This commit is contained in:
committed by
Pierre Donias
parent
c613b4cab3
commit
449dd2998b
@@ -0,0 +1,23 @@
|
||||
## Select
|
||||
### Usage
|
||||
```js
|
||||
<Select
|
||||
value={this.state.selectedOption}
|
||||
onChange={selectedOption => this.setState({ selectedOption })}
|
||||
optionRenderer={option => option.label}
|
||||
options={[
|
||||
{ value: 'foo', label: 'Foo' },
|
||||
{ value: 'bar', label: 'Bar' },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
### Props
|
||||
|
||||
Name | Type | Default | Description
|
||||
------- | --- | --- | ---------
|
||||
`options` | `Array` of `Object`s | | Required. Options that can be selected. `label` and `value` properties are required for each option.
|
||||
`multi`| `Boolean` | `false` | Allow to select multiple values.
|
||||
`value` | `Object` or `Array` of `Object`s when `multi` is `true` | | Required. Current value.
|
||||
`onChange` | `Function` | | Manage the changed value. Parameters: selected value(s).
|
||||
`optionRenderer` | `Function` | | Manage option display. Parameter: an element of `options`.
|
||||
`children` | | | How the component will be rendered. Will fallback to `optionRenderer` if not used.
|
||||
@@ -1,24 +1,15 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
findKey,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isString,
|
||||
map,
|
||||
pick,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
import { isEmpty, isFunction, isString, map, pick, startsWith } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
import { Select as FormSelect, SizeInput } from '../form'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
@@ -341,74 +332,94 @@ export class Number extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export class Select extends Editable {
|
||||
class SimpleSelect_ extends Editable {
|
||||
static propTypes = {
|
||||
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
||||
.isRequired,
|
||||
renderer: PropTypes.func,
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (
|
||||
props.value !== this.props.value ||
|
||||
props.options !== this.props.options
|
||||
) {
|
||||
this.setState({
|
||||
valueKey: findKey(props.options, option => option === props.value),
|
||||
})
|
||||
}
|
||||
optionRenderer: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.oneOf([null]), PropTypes.object]),
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.props.options[this.state.valueKey]
|
||||
return this.state.value === undefined ? this.props.value : this.state.value
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
this.setState({ valueKey: getEventValue(event) }, this._save)
|
||||
}
|
||||
|
||||
_optionToJsx = (option, key) => {
|
||||
const { renderer } = this.props
|
||||
_onChange = value => this.setState({ value }, this._save)
|
||||
|
||||
_renderDisplay() {
|
||||
const { children, optionRenderer, value } = this.props
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{renderer ? renderer(option) : option}
|
||||
</option>
|
||||
children || (
|
||||
<span>
|
||||
{optionRenderer !== undefined
|
||||
? optionRenderer(value)
|
||||
: value != null
|
||||
? value.label
|
||||
: _('noValue')}
|
||||
</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
_onEditionMount = ref => {
|
||||
// Seems to work in Google Chrome (not in Firefox)
|
||||
ref && ref.dispatchEvent(new window.MouseEvent('mousedown'))
|
||||
_renderEdition = () => (
|
||||
<FormSelect
|
||||
{...this.props}
|
||||
autoFocus
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
openOnFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
class MultiSelect_ extends Editable {
|
||||
static propTypes = {
|
||||
optionRenderer: PropTypes.func,
|
||||
value: PropTypes.array,
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.state.nextValue === undefined
|
||||
? this.props.value
|
||||
: this.state.nextValue
|
||||
}
|
||||
|
||||
_renderDisplay() {
|
||||
const { children, renderer, value } = this.props
|
||||
|
||||
return children || <span>{renderer ? renderer(value) : value}</span>
|
||||
}
|
||||
|
||||
_renderEdition() {
|
||||
const { saving, valueKey } = this.state
|
||||
const { options } = this.props
|
||||
const { children, optionRenderer, value } = this.props
|
||||
|
||||
return (
|
||||
<select
|
||||
autoFocus
|
||||
className={classNames('form-control', styles.select)}
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref={this._onEditionMount}
|
||||
value={valueKey}
|
||||
>
|
||||
{map(options, this._optionToJsx)}
|
||||
</select>
|
||||
children || (
|
||||
<span>
|
||||
{!isEmpty(value)
|
||||
? map(value, optionRenderer || 'label').join(', ')
|
||||
: _('noValue')}
|
||||
</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
_onBlur = () => {
|
||||
this._save().then(() => this.setState({ nextValue: undefined }))
|
||||
}
|
||||
|
||||
_renderEdition = () => (
|
||||
<FormSelect
|
||||
{...this.props}
|
||||
autoFocus
|
||||
multi
|
||||
onBlur={this._onBlur}
|
||||
onChange={this.linkState('nextValue')}
|
||||
openOnFocus
|
||||
value={this.state.nextValue || this.props.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const Select = ({ multi, ...props }) =>
|
||||
multi ? <MultiSelect_ {...props} /> : <SimpleSelect_ {...props} />
|
||||
|
||||
Select.defaultProps = { multi: false }
|
||||
Select.propTypes = { multi: PropTypes.bool }
|
||||
|
||||
const MAP_TYPE_SELECT = {
|
||||
host: SelectHost,
|
||||
ip: SelectIp,
|
||||
|
||||
@@ -19,6 +19,7 @@ const messages = {
|
||||
editableClickPlaceholder: 'Click to edit',
|
||||
browseFiles: 'Browse files',
|
||||
showLogs: 'Show logs',
|
||||
noValue: 'None',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
|
||||
@@ -11,7 +11,7 @@ import Tooltip from 'tooltip'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore, noop } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { error } from 'notification'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { Select, Number } from 'editable'
|
||||
@@ -165,14 +165,26 @@ class PifItemMode extends Component {
|
||||
getIpv4ConfigModes().then(configModes => this.setState({ configModes }))
|
||||
}
|
||||
|
||||
_configIp = mode => reconfigureIp(this.props.pif, mode)
|
||||
_configIp = mode => mode != null && reconfigureIp(this.props.pif, mode.value)
|
||||
|
||||
_getOptions = createSelector(
|
||||
() => this.state.configModes,
|
||||
configModes => configModes.map(mode => ({ label: mode, value: mode }))
|
||||
)
|
||||
|
||||
_getValue = createSelector(
|
||||
() => this.props.pif.mode,
|
||||
mode => ({ label: mode, value: mode })
|
||||
)
|
||||
|
||||
render() {
|
||||
const { pif } = this.props
|
||||
const { configModes } = this.state
|
||||
return (
|
||||
<Select onChange={this._configIp} options={configModes} value={pif.mode}>
|
||||
{pif.mode}
|
||||
<Select
|
||||
onChange={this._configIp}
|
||||
options={this._getOptions()}
|
||||
value={this._getValue()}
|
||||
>
|
||||
{this.props.pif.mode}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import Page from '../page'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { cloneElement } from 'react'
|
||||
import VmActionBar from './action-bar'
|
||||
import { Select, Text } from 'editable'
|
||||
import { Host } from 'render-xo-item'
|
||||
import { Text, XoSelect } from 'editable'
|
||||
import { assign, isEmpty, map, pick } from 'lodash'
|
||||
import { editVm, fetchVmStats, isVmRunning, migrateVm } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -69,8 +70,6 @@ import TabAdvanced from './tab-advanced'
|
||||
|
||||
const getVmTotalDiskSpace = createSumBy(createGetVmDisks(getVm), 'size')
|
||||
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
|
||||
return (state, props) => {
|
||||
const vm = getVm(state, props)
|
||||
if (!vm) {
|
||||
@@ -80,7 +79,6 @@ import TabAdvanced from './tab-advanced'
|
||||
return {
|
||||
checkPermissions: getCheckPermissions(state, props),
|
||||
container: getContainer(state, props),
|
||||
hosts: getHosts(state, props),
|
||||
isAdmin: isAdmin(state, props),
|
||||
pool: getPool(state, props),
|
||||
srs: getSrs(state, props),
|
||||
@@ -166,10 +164,8 @@ export default class Vm extends BaseComponent {
|
||||
_setNameLabel = nameLabel => editVm(this.props.vm, { name_label: nameLabel })
|
||||
_migrateVm = host => migrateVm(this.props.vm, host)
|
||||
|
||||
_selectOptionRenderer = option => option.name_label
|
||||
|
||||
header() {
|
||||
const { vm, container, pool, hosts } = this.props
|
||||
const { vm, container, pool } = this.props
|
||||
if (!vm) {
|
||||
return <Icon icon='loading' />
|
||||
}
|
||||
@@ -198,17 +194,14 @@ export default class Vm extends BaseComponent {
|
||||
{vm.power_state === 'Running' && container && (
|
||||
<span>
|
||||
<span> - </span>
|
||||
<Select
|
||||
<XoSelect
|
||||
onChange={this._migrateVm}
|
||||
options={hosts}
|
||||
renderer={this._selectOptionRenderer}
|
||||
useLongClick
|
||||
value={container}
|
||||
xoType='host'
|
||||
>
|
||||
<Link to={`/${container.type}s/${container.id}`}>
|
||||
{container.name_label}
|
||||
</Link>
|
||||
</Select>
|
||||
<Host id={container.id} pool={false} link />
|
||||
</XoSelect>
|
||||
</span>
|
||||
)}{' '}
|
||||
{pool && (
|
||||
|
||||
Reference in New Issue
Block a user