webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js in katello-4.0.3 vs webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js in katello-4.1.0.rc1
- old
+ new
@@ -1,29 +1,40 @@
import React, { useState, useEffect } from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
-import { Bullseye, Split, SplitItem } from '@patternfly/react-core';
+import {
+ Bullseye,
+ Split,
+ SplitItem,
+ Button,
+ ActionList,
+ ActionListItem,
+ Dropdown,
+ DropdownItem,
+ KebabToggle,
+} from '@patternfly/react-core';
import { TableVariant, fitContent } from '@patternfly/react-table';
import { STATUS } from 'foremanReact/constants';
import { translate as __ } from 'foremanReact/common/I18n';
import { urlBuilder } from 'foremanReact/common/urlHelpers';
import PropTypes from 'prop-types';
import TableWrapper from '../../../../components/Table/TableWrapper';
import onSelect from '../../../../components/Table/helpers';
-import { getContentViewRepositories, getRepositoryTypes } from '../ContentViewDetailActions';
+import { getContentViewRepositories, getRepositoryTypes, updateContentView } from '../ContentViewDetailActions';
import {
selectCVRepos,
selectCVReposStatus,
selectCVReposError,
selectRepoTypes,
selectRepoTypesStatus,
+ selectCVDetails,
} from '../ContentViewDetailSelectors';
import { ADDED, NOT_ADDED, ALL_STATUSES } from '../../ContentViewsConstants';
import ContentCounts from './ContentCounts';
import LastSync from './LastSync';
-import RepoAddedStatus from './RepoAddedStatus';
import RepoIcon from './RepoIcon';
+import AddedStatusLabel from '../../../../components/AddedStatusLabel';
import SelectableDropdown from '../../../../components/SelectableDropdown';
import { capitalize } from '../../../../utils/helpers';
const allRepositories = 'All repositories';
@@ -39,18 +50,21 @@
const response = useSelector(state => selectCVRepos(state, cvId), shallowEqual);
const status = useSelector(state => selectCVReposStatus(state, cvId), shallowEqual);
const error = useSelector(state => selectCVReposError(state, cvId), shallowEqual);
const repoTypesResponse = useSelector(state => selectRepoTypes(state), shallowEqual);
const repoTypesStatus = useSelector(state => selectRepoTypesStatus(state), shallowEqual);
+ const details = useSelector(state => selectCVDetails(state, cvId), shallowEqual);
const [rows, setRows] = useState([]);
const [metadata, setMetadata] = useState({});
const [searchQuery, updateSearchQuery] = useState('');
const [typeSelected, setTypeSelected] = useState(allRepositories);
const [statusSelected, setStatusSelected] = useState(ALL_STATUSES);
// repoTypes object format: [displayed_value]: API_value
const [repoTypes, setRepoTypes] = useState({});
+ const [bulkActionOpen, setBulkActionOpen] = useState(false);
+ const [bulkActionEnabled, setBulkActionEnabled] = useState(false);
const columnHeaders = [
{ title: __('Type'), transforms: [fitContent] },
__('Name'),
__('Product'),
@@ -79,26 +93,18 @@
{ title: <a href={urlBuilder(`products/${productId}/repositories`, '', id)}>{name}</a> },
productName,
{ title: <LastSync {...{ lastSyncWords, lastSync }} /> },
{ title: <ContentCounts {...{ counts, productId }} repoId={id} /> },
{
- title: <RepoAddedStatus added={addedToCV || statusSelected === ADDED} />,
+ title: <AddedStatusLabel added={addedToCV || statusSelected === ADDED} />,
},
];
-
- newRows.push({ cells });
+ newRows.push({ repoId: id, cells });
});
return newRows;
};
- const getCVReposWithOptions = (params = {}) => {
- const allParams = { ...params };
- if (typeSelected !== 'All repositories') allParams.content_type = repoTypes[typeSelected];
-
- return getContentViewRepositories(cvId, allParams, statusSelected);
- };
-
useEffect(() => {
const { results, ...meta } = response;
setMetadata(meta);
if (!loading && results) {
@@ -109,10 +115,15 @@
useEffect(() => {
dispatch(getRepositoryTypes());
}, []);
+ useEffect(() => {
+ const rowsAreSelected = rows.some(row => row.selected);
+ setBulkActionEnabled(rowsAreSelected);
+ }, [rows]);
+
// Get repo type filter selections dynamically from the API
useEffect(() => {
if (repoTypesStatus === STATUS.RESOLVED && repoTypesResponse) {
const allRepoTypes = {};
allRepoTypes[allRepositories] = 'all';
@@ -124,16 +135,80 @@
});
setRepoTypes(allRepoTypes);
}
}, [JSON.stringify(repoTypesResponse), repoTypesStatus]);
+ const toggleBulkAction = () => {
+ setBulkActionOpen(!bulkActionOpen);
+ };
+
+ const onAdd = (repos) => {
+ const { repository_ids: repositoryIds = [] } = details;
+ dispatch(updateContentView(cvId, { repository_ids: repositoryIds.concat(repos) }));
+ };
+
+ const onRemove = (repos) => {
+ const reposToDelete = [].concat(repos);
+ const { repository_ids: repositoryIds = [] } = details;
+ const deletedRepos = repositoryIds.filter(x => !reposToDelete.includes(x));
+ dispatch(updateContentView(cvId, { repository_ids: deletedRepos }));
+ };
+
+ const addBulk = () => {
+ const reposToAdd = [];
+ rows.forEach(row => row.selected && reposToAdd.push(row.repoId));
+ onAdd(reposToAdd);
+ };
+
+ const removeBulk = () => {
+ const reposToDelete = [];
+ rows.forEach(row => row.selected && reposToDelete.push(row.repoId));
+ onRemove(reposToDelete);
+ };
+
+ const actionResolver = (rowData, { _rowIndex }) => {
+ if (rowData.parent || rowData.compoundParent || rowData.noactions) return null;
+ const { repository_ids: repositoryIds } = details;
+ return [
+ {
+ title: 'Add',
+ isDisabled: repositoryIds && repositoryIds.includes(rowData.repoId),
+ onClick: (_event, rowId, rowInfo) => {
+ onAdd(rowInfo.repoId);
+ },
+ },
+ {
+ title: 'Remove',
+ isDisabled: repositoryIds && !repositoryIds.includes(rowData.repoId),
+ onClick: (_event, rowId, rowInfo) => {
+ onRemove(rowInfo.repoId);
+ },
+ },
+ ];
+ };
+
+ const getCVReposWithOptions = (params = {}) => {
+ const allParams = { ...params };
+ if (typeSelected !== 'All repositories') allParams.content_type = repoTypes[typeSelected];
+
+ return getContentViewRepositories(cvId, allParams, statusSelected);
+ };
+
const emptyContentTitle = __("You currently don't have any repositories to add to this content view.");
const emptyContentBody = __('Please add some repositories.'); // needs link
const emptySearchTitle = __('No matching repositories found');
const emptySearchBody = __('Try changing your search settings.');
const activeFilters = (typeSelected && typeSelected !== allRepositories) ||
(statusSelected && statusSelected !== ALL_STATUSES);
+ const dropdownItems = [
+ <DropdownItem aria-label="bulk_add" key="bulk_add" isDisabled={!bulkActionEnabled} component="button" onClick={addBulk}>
+ Add
+ </DropdownItem>,
+ <DropdownItem aria-label="bulk_remove" key="bulk_remove" isDisabled={!bulkActionEnabled} component="button" onClick={removeBulk}>
+ Remove
+ </DropdownItem>,
+ ];
return (
<TableWrapper
{...{
rows,
@@ -142,10 +217,11 @@
emptyContentBody,
emptySearchTitle,
emptySearchBody,
searchQuery,
updateSearchQuery,
+ actionResolver,
error,
status,
activeFilters,
}}
onSelect={onSelect(rows, setRows)}
@@ -173,9 +249,26 @@
title="Status"
selected={statusSelected}
setSelected={setStatusSelected}
placeholderText="Status"
/>
+ </SplitItem>
+ <SplitItem>
+ <ActionList>
+ <ActionListItem>
+ <Button onClick={addBulk} isDisabled={!bulkActionEnabled} variant="secondary" aria-label="add_repositories">
+ Add repositories
+ </Button>
+ </ActionListItem>
+ <ActionListItem>
+ <Dropdown
+ toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />}
+ isOpen={bulkActionOpen}
+ isPlain
+ dropdownItems={dropdownItems}
+ />
+ </ActionListItem>
+ </ActionList>
</SplitItem>
</Split>
</TableWrapper>
);
};