feat(xo-web/sorted-table): ability to collapse actions (#5311)

See #5148
This commit is contained in:
badrAZ 2020-10-07 11:13:10 +02:00 committed by GitHub
parent 0b8a7c0d09
commit a186672447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,10 +1,13 @@
import * as CM from 'complex-matcher' import * as CM from 'complex-matcher'
import _ from 'intl' import _ from 'intl'
import classNames from 'classnames' import classNames from 'classnames'
import defined from '@xen-orchestra/defined' import defined, { ifDef } from '@xen-orchestra/defined'
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import Shortcuts from 'shortcuts' import Shortcuts from 'shortcuts'
import { Dropdown, MenuItem } from 'react-bootstrap-4/lib'
import { Portal } from 'react-overlays' import { Portal } from 'react-overlays'
import { Set } from 'immutable' import { Set } from 'immutable'
import { injectState, provideState } from 'reaclette' import { injectState, provideState } from 'reaclette'
@ -15,6 +18,7 @@ import {
findIndex, findIndex,
forEach, forEach,
get as getProperty, get as getProperty,
groupBy,
isEmpty, isEmpty,
map, map,
sortBy, sortBy,
@ -26,11 +30,15 @@ import ButtonGroup from '../button-group'
import Component from '../base-component' import Component from '../base-component'
import decorate from '../apply-decorators' import decorate from '../apply-decorators'
import Icon from '../icon' import Icon from '../icon'
import logError from '../log-error'
import Pagination from '../pagination' import Pagination from '../pagination'
import SingleLineRow from '../single-line-row' import SingleLineRow from '../single-line-row'
import TableFilter from '../search-bar' import TableFilter from '../search-bar'
import UserError from '../user-error'
import { BlockLink } from '../link' import { BlockLink } from '../link'
import { Container, Col } from '../grid' import { Container, Col } from '../grid'
import { error as _error } from '../notification'
import { generateId } from '../reaclette-utils'
import { import {
createCollectionWrapper, createCollectionWrapper,
createCounter, createCounter,
@ -120,6 +128,7 @@ const actionsShape = PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
// groupedActions: the function will be called with an array of the selected items in parameters // groupedActions: the function will be called with an array of the selected items in parameters
// individualActions: the function will be called with the related item in parameters // individualActions: the function will be called with the related item in parameters
collapsed: PropTypes.bool,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
handler: PropTypes.func.isRequired, handler: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
@ -159,6 +168,80 @@ const Action = decorate([
), ),
]) ])
const handleFnProps = (prop, items, userData) =>
typeof prop === 'function' ? prop(items, userData) : prop
const CollapsedActions = decorate([
withRouter,
provideState({
effects: {
async execute(state, { handler, label, redirectOnSuccess }) {
try {
await handler()
ifDef(redirectOnSuccess, this.props.router.push)
} catch (error) {
// ignore when undefined because it usually means that the action has been canceled
if (error !== undefined) {
if (error instanceof UserError) {
_error(error.title, error.body)
} else {
logError(error)
_error(label, defined(error.message, String(error)))
}
}
}
},
},
computed: {
dropdownId: generateId,
actions: (_, { actions, items, userData }) =>
actions.map(
({ disabled, grouped, handler, icon, label, redirectOnSuccess }) => {
const actionItems =
Array.isArray(items) || !grouped ? items : [items]
return {
disabled: handleFnProps(disabled, actionItems, userData),
handler: () => handler(actionItems, userData),
icon: handleFnProps(icon, actionItems, userData),
label: handleFnProps(label, actionItems, userData),
redirectOnSuccess: handleFnProps(
redirectOnSuccess,
actionItems,
userData
),
}
}
),
},
}),
injectState,
({ state, effects }) => (
<Dropdown id={state.dropdownId}>
<DropdownToggle bsSize='small' bsStyle='secondary' />
<DropdownMenu className='dropdown-menu-right'>
{state.actions.map((action, key) => (
<MenuItem
disabled={action.disabled}
key={key}
onClick={() => effects.execute(action)}
>
<Icon icon={action.icon} /> {action.label}
</MenuItem>
))}
</DropdownMenu>
</Dropdown>
),
])
CollapsedActions.propTypes = {
actions: PropTypes.shape({
...actionsShape,
grouped: PropTypes.bool,
}),
items: PropTypes.any,
userData: PropTypes.any,
}
const LEVELS = [undefined, 'primary', 'warning', 'danger'] const LEVELS = [undefined, 'primary', 'warning', 'danger']
// page number and sort info are optional for backward compatibility // page number and sort info are optional for backward compatibility
const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(?:_(desc|asc))?)?-)?(.*)$/ const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(?:_(desc|asc))?)?-)?(.*)$/
@ -196,6 +279,7 @@ class SortedTable extends Component {
actions: PropTypes.arrayOf( actions: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
// regroup individual actions and grouped actions // regroup individual actions and grouped actions
collapsed: PropTypes.bool,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
handler: PropTypes.func.isRequired, handler: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
@ -618,11 +702,14 @@ class SortedTable extends Component {
() => this.props.groupedActions, () => this.props.groupedActions,
() => this.props.actions, () => this.props.actions,
(groupedActions, actions) => (groupedActions, actions) =>
sortBy( groupBy(
groupedActions !== undefined && actions !== undefined sortBy(
? groupedActions.concat(actions) groupedActions !== undefined && actions !== undefined
: groupedActions || actions, ? groupedActions.concat(actions)
action => LEVELS.indexOf(action.level) : groupedActions || actions,
action => LEVELS.indexOf(action.level)
),
action => (action.collapsed ? 'secondary' : 'primary')
) )
) )
@ -631,6 +718,7 @@ class SortedTable extends Component {
() => this.props.actions, () => this.props.actions,
(individualActions, actions) => { (individualActions, actions) => {
const normalizedActions = map(actions, a => ({ const normalizedActions = map(actions, a => ({
collapsed: a.collapsed,
disabled: disabled:
a.individualDisabled !== undefined a.individualDisabled !== undefined
? a.individualDisabled ? a.individualDisabled
@ -644,11 +732,14 @@ class SortedTable extends Component {
redirectOnSuccess: a.redirectOnSuccess, redirectOnSuccess: a.redirectOnSuccess,
})) }))
return sortBy( return groupBy(
individualActions !== undefined && actions !== undefined sortBy(
? individualActions.concat(normalizedActions) individualActions !== undefined && actions !== undefined
: individualActions || normalizedActions, ? individualActions.concat(normalizedActions)
action => LEVELS.indexOf(action.level) : individualActions || normalizedActions,
action => LEVELS.indexOf(action.level)
),
action => (action.collapsed ? 'secondary' : 'primary')
) )
} }
) )
@ -689,17 +780,29 @@ class SortedTable extends Component {
/> />
</td> </td>
) )
const actionsColumn = hasIndividualActions && (
<td> let actionsColumn
<div className='pull-right'> if (hasIndividualActions) {
<ButtonGroup> const { primary, secondary } = this._getIndividualActions()
{map(this._getIndividualActions(), (props, key) => ( actionsColumn = (
<Action {...props} items={item} key={key} userData={userData} /> <td>
))} <div className='pull-right'>
</ButtonGroup> <ButtonGroup>
</div> {map(primary, (props, key) => (
</td> <Action {...props} items={item} key={key} userData={userData} />
) ))}
{secondary !== undefined && (
<CollapsedActions
actions={secondary}
items={item}
userData={userData}
/>
)}
</ButtonGroup>
</div>
</td>
)
}
return rowLink != null ? ( return rowLink != null ? (
<BlockLink <BlockLink
@ -828,7 +931,7 @@ class SortedTable extends Component {
{(nSelectedItems !== 0 || all) && ( {(nSelectedItems !== 0 || all) && (
<div className='pull-right'> <div className='pull-right'>
<ButtonGroup> <ButtonGroup>
{map(groupedActions, (props, key) => ( {map(groupedActions.primary, (props, key) => (
<Action <Action
{...props} {...props}
key={key} key={key}
@ -836,6 +939,13 @@ class SortedTable extends Component {
userData={userData} userData={userData}
/> />
))} ))}
{groupedActions.secondary !== undefined && (
<CollapsedActions
actions={groupedActions.secondary}
items={this._getSelectedItems()}
userData={userData}
/>
)}
</ButtonGroup> </ButtonGroup>
</div> </div>
)} )}