diff --git a/packages/xo-web/src/common/sorted-table/index.js b/packages/xo-web/src/common/sorted-table/index.js
index b273a287a..19c0f699b 100644
--- a/packages/xo-web/src/common/sorted-table/index.js
+++ b/packages/xo-web/src/common/sorted-table/index.js
@@ -1,10 +1,13 @@
import * as CM from 'complex-matcher'
import _ from 'intl'
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 React from 'react'
import Shortcuts from 'shortcuts'
+import { Dropdown, MenuItem } from 'react-bootstrap-4/lib'
import { Portal } from 'react-overlays'
import { Set } from 'immutable'
import { injectState, provideState } from 'reaclette'
@@ -15,6 +18,7 @@ import {
findIndex,
forEach,
get as getProperty,
+ groupBy,
isEmpty,
map,
sortBy,
@@ -26,11 +30,15 @@ import ButtonGroup from '../button-group'
import Component from '../base-component'
import decorate from '../apply-decorators'
import Icon from '../icon'
+import logError from '../log-error'
import Pagination from '../pagination'
import SingleLineRow from '../single-line-row'
import TableFilter from '../search-bar'
+import UserError from '../user-error'
import { BlockLink } from '../link'
import { Container, Col } from '../grid'
+import { error as _error } from '../notification'
+import { generateId } from '../reaclette-utils'
import {
createCollectionWrapper,
createCounter,
@@ -120,6 +128,7 @@ const actionsShape = PropTypes.arrayOf(
PropTypes.shape({
// 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
+ collapsed: PropTypes.bool,
disabled: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
handler: PropTypes.func.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 }) => (
+