diff --git a/package-lock.json b/package-lock.json
index 340df233..afb34325 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "react-invenio-forms",
- "version": "2.0.0",
+ "version": "2.4.0",
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.5.0",
diff --git a/src/lib/elements/bulk_actions/SearchResultsBulkActions.js b/src/lib/elements/bulk_actions/SearchResultsBulkActions.js
new file mode 100644
index 00000000..ec773b58
--- /dev/null
+++ b/src/lib/elements/bulk_actions/SearchResultsBulkActions.js
@@ -0,0 +1,97 @@
+/*
+ * This file is part of Invenio.
+ * Copyright (C) 2022 CERN.
+ *
+ * Invenio is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT License; see LICENSE file for more details.
+ */
+
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Checkbox, Dropdown } from "semantic-ui-react";
+import { BulkActionsContext } from "./context";
+import _pickBy from "lodash/pickBy";
+
+export class SearchResultsBulkActions extends Component {
+ constructor(props) {
+ super(props);
+ const { allSelected } = this.props;
+ this.state = { allSelectedChecked: allSelected };
+ }
+
+ componentDidMount() {
+ const { allSelected } = this.context;
+ // eslint-disable-next-line react/no-did-mount-set-state
+ this.setState({ allSelectedChecked: allSelected });
+ }
+
+ static contextType = BulkActionsContext;
+
+ handleOnChange = () => {
+ const { setAllSelected, allSelected } = this.context;
+ this.setState({ allSelectedChecked: !allSelected });
+ setAllSelected(!allSelected, true);
+ };
+
+ handleActionOnChange = (e, { value, ...props }) => {
+ if (!value) return;
+
+ const { optionSelectionCallback } = this.props;
+
+ const { selectedCount, bulkActionContext } = this.context;
+ const selected = _pickBy(bulkActionContext, ({ selected }) => selected === true);
+ optionSelectionCallback(value, selected, selectedCount);
+ };
+
+ render() {
+ const { bulkDropdownOptions } = this.props;
+ const { allSelectedChecked } = this.state;
+ const { allSelected, selectedCount } = this.context;
+
+ const noneSelected = selectedCount === 0;
+
+ const dropdownOptions = bulkDropdownOptions.map(({ key, value, text }) => ({
+ key: key,
+ value: value,
+ text: text,
+ disabled: noneSelected,
+ }));
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+SearchResultsBulkActions.propTypes = {
+ bulkDropdownOptions: PropTypes.array.isRequired,
+ allSelected: PropTypes.bool,
+ optionSelectionCallback: PropTypes.func.isRequired,
+};
+
+SearchResultsBulkActions.defaultProps = {
+ allSelected: false,
+};
+
+export default Overridable.component(
+ "SearchResultsBulkActions",
+ SearchResultsBulkActions
+);
diff --git a/src/lib/elements/bulk_actions/SearchResultsBulkActionsManager.js b/src/lib/elements/bulk_actions/SearchResultsBulkActionsManager.js
new file mode 100644
index 00000000..508f7659
--- /dev/null
+++ b/src/lib/elements/bulk_actions/SearchResultsBulkActionsManager.js
@@ -0,0 +1,81 @@
+import { BulkActionsContext } from "./context";
+import React, { Component } from "react";
+import _hasIn from "lodash/hasIn";
+import PropTypes from "prop-types";
+
+class SearchResultsBulkActionsManager extends Component {
+ constructor(props) {
+ super(props);
+
+ this.selected = {};
+ this.state = { allSelected: false, selectedCount: 0 };
+ }
+
+ addToSelected = (rowId, data) => {
+ const { selectedCount } = this.state;
+ if (_hasIn(this.selected, `${rowId}`)) {
+ this.selected[rowId].selected = !this.selected[rowId].selected;
+ } else {
+ this.selected[rowId].selected = true;
+ this.selected[rowId].data = data;
+ }
+
+ if (!this.selected[rowId].selected) {
+ this.setAllSelected(false);
+ this.setSelectedCount(selectedCount - 1);
+ } else {
+ const updatedCount = selectedCount + 1;
+ this.setSelectedCount(updatedCount);
+ if (Object.keys(this.selected).length === updatedCount) {
+ this.setAllSelected(true);
+ }
+ }
+ };
+
+ setSelectedCount = (count) => {
+ this.setState({ selectedCount: count });
+ };
+
+ setAllSelected = (val, global = false) => {
+ this.setState({ allSelected: val });
+ if (global) {
+ for (const [key] of Object.entries(this.selected)) {
+ this.selected[key].selected = val;
+ }
+ if (val) {
+ this.setSelectedCount(Object.keys(this.selected).length);
+ } else {
+ this.setSelectedCount(0);
+ }
+ }
+ };
+
+ render() {
+ const { children } = this.props;
+ const { allSelected, selectedCount } = this.state;
+ return (
+
+ {children}
+
+ );
+ }
+}
+
+SearchResultsBulkActionsManager.contextType = BulkActionsContext;
+
+SearchResultsBulkActionsManager.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export default Overridable.component(
+ "SearchResultsBulkActionsManager",
+ SearchResultsBulkActionsManager
+);
diff --git a/src/lib/elements/bulk_actions/SearchResultsRowCheckbox.js b/src/lib/elements/bulk_actions/SearchResultsRowCheckbox.js
new file mode 100644
index 00000000..1237751b
--- /dev/null
+++ b/src/lib/elements/bulk_actions/SearchResultsRowCheckbox.js
@@ -0,0 +1,63 @@
+import { BulkActionsContext } from "./context";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Checkbox } from "semantic-ui-react";
+import _hasIn from "lodash/hasIn";
+
+export class SearchResultsRowCheckbox extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { isChecked: false };
+ }
+
+ componentDidMount() {
+ this.subscribeToContext();
+ const { bulkActionContext, allSelected } = this.context;
+ // eslint-disable-next-line react/no-did-mount-set-state
+ this.setState({
+ isChecked: this.isChecked(bulkActionContext, allSelected),
+ });
+ }
+
+ static contextType = BulkActionsContext;
+
+ isChecked = (bulkActionContext, allSelected) => {
+ const { rowId } = this.props;
+ if (_hasIn(bulkActionContext, `${rowId}`) || allSelected) {
+ return bulkActionContext[rowId].selected;
+ }
+ return false;
+ };
+
+ subscribeToContext = () => {
+ const { rowId, data } = this.props;
+ const { allSelected, bulkActionContext } = this.context;
+ if (!_hasIn(bulkActionContext, `${rowId}`)) {
+ bulkActionContext[rowId] = { selected: allSelected, data: data };
+ }
+ };
+
+ handleOnChange = () => {
+ const { addToSelected } = this.context;
+ const { rowId, data } = this.props;
+ const { isChecked } = this.state;
+ this.setState({ isChecked: !isChecked });
+ addToSelected(rowId, data);
+ };
+
+ render() {
+ const { bulkActionContext, allSelected } = this.context;
+ return (
+
+ );
+ }
+}
+
+SearchResultsRowCheckbox.propTypes = {
+ rowId: PropTypes.string.isRequired,
+ data: PropTypes.object.isRequired,
+};
diff --git a/src/lib/elements/bulk_actions/context.js b/src/lib/elements/bulk_actions/context.js
new file mode 100644
index 00000000..4e7d7e68
--- /dev/null
+++ b/src/lib/elements/bulk_actions/context.js
@@ -0,0 +1,9 @@
+import React from "react";
+
+export const BulkActionsContext = React.createContext({
+ bulkActionContext: {},
+ addToSelected: () => {},
+ allSelected: false,
+ setAllSelected: () => {},
+ selectedCount: 0,
+});
diff --git a/src/lib/elements/bulk_actions/index.js b/src/lib/elements/bulk_actions/index.js
new file mode 100644
index 00000000..2d0fbec4
--- /dev/null
+++ b/src/lib/elements/bulk_actions/index.js
@@ -0,0 +1,4 @@
+export { BulkActionsContext } from "./context";
+export { SearchResultsBulkActions } from "./SearchResultsBulkActions";
+export { default as SearchResultsBulkActionsManager } from "./SearchResultsBulkActionsManager";
+export { SearchResultsRowCheckbox } from "./SearchResultsRowCheckbox";
diff --git a/src/lib/elements/contrib/index.js b/src/lib/elements/contrib/index.js
new file mode 100644
index 00000000..7b7e8980
--- /dev/null
+++ b/src/lib/elements/contrib/index.js
@@ -0,0 +1,9 @@
+/*
+ * // This file is part of invenio-app-rdm
+ * // Copyright (C) 2023 CERN.
+ * //
+ * // invenio-app-rdm is free software; you can redistribute it and/or modify it
+ * // under the terms of the MIT License; see LICENSE file for more details.
+ */
+
+export * from "./invenioRDM";
diff --git a/src/lib/elements/contrib/invenioRDM/groups/index.js b/src/lib/elements/contrib/invenioRDM/groups/index.js
new file mode 100644
index 00000000..f568af83
--- /dev/null
+++ b/src/lib/elements/contrib/invenioRDM/groups/index.js
@@ -0,0 +1,7 @@
+/*
+ * // This file is part of React-Invenio-Forms
+ * // Copyright (C) 2023 CERN.
+ * //
+ * // React-Invenio-Forms is free software; you can redistribute it and/or modify it
+ * // under the terms of the MIT License; see LICENSE file for more details.
+ */
diff --git a/src/lib/elements/contrib/invenioRDM/index.js b/src/lib/elements/contrib/invenioRDM/index.js
new file mode 100644
index 00000000..2c6d0111
--- /dev/null
+++ b/src/lib/elements/contrib/invenioRDM/index.js
@@ -0,0 +1,10 @@
+/*
+ * // This file is part of React-Invenio-Forms
+ * // Copyright (C) 2023 CERN.
+ * //
+ * // React-Invenio-Forms is free software; you can redistribute it and/or modify it
+ * // under the terms of the MIT License; see LICENSE file for more details.
+ */
+
+export * from "./users";
+export * from "./groups";
diff --git a/src/lib/elements/contrib/invenioRDM/users/UserListItemCompact.js b/src/lib/elements/contrib/invenioRDM/users/UserListItemCompact.js
new file mode 100644
index 00000000..c1d94f90
--- /dev/null
+++ b/src/lib/elements/contrib/invenioRDM/users/UserListItemCompact.js
@@ -0,0 +1,46 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Image } from "../../../Image";
+import { Item, Label } from "semantic-ui-react";
+
+export class UserListItemCompact extends Component {
+ render() {
+ const { id, user, linkToDetailView } = this.props;
+ const name = user.profile.full_name || user.profile.email || user.profile.username;
+ return (
+ -
+
+
+
+ {linkToDetailView ? (
+
+ {name}
+
+ ) : (
+ {name}
+ )}
+ {user.type === "group" && }
+ {user.is_current_user && (
+
+ )}
+
+
+
{user.profile.affiliations}
+
+
+
+ );
+ }
+}
+
+UserListItemCompact.propTypes = {
+ user: PropTypes.object.isRequired,
+ id: PropTypes.string.isRequired,
+ linkToDetailView: PropTypes.string,
+};
+
+UserListItemCompact.defaultProps = {
+ linkToDetailView: undefined,
+};
diff --git a/src/lib/elements/contrib/invenioRDM/users/index.js b/src/lib/elements/contrib/invenioRDM/users/index.js
new file mode 100644
index 00000000..846f8ce7
--- /dev/null
+++ b/src/lib/elements/contrib/invenioRDM/users/index.js
@@ -0,0 +1,9 @@
+/*
+ * // This file is part of React-Invenio-Forms
+ * // Copyright (C) 2023 CERN.
+ * //
+ * // React-Invenio-Forms is free software; you can redistribute it and/or modify it
+ * // under the terms of the MIT License; see LICENSE file for more details.
+ */
+
+export { UserListItemCompact } from "./UserListItemCompact";
diff --git a/src/lib/elements/index.js b/src/lib/elements/index.js
index 745dc16e..d01b8850 100644
--- a/src/lib/elements/index.js
+++ b/src/lib/elements/index.js
@@ -8,6 +8,8 @@
* This folder contains general purpose reusable components.
*/
export * from "./accessibility";
+export * from "./contrib";
+export * from "./bulk_actions";
export { Image } from "./Image";
export { GridResponsiveSidebarColumn } from "./GridResponsiveSidebarColumn";
export { ErrorMessage } from "./ErrorMessage";