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> ); };