webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js in katello-4.4.2.2 vs webpack/scenes/ContentViews/Details/Repositories/ContentViewRepositories.js in katello-4.5.0.rc1

- old
+ new

@@ -1,43 +1,71 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { + useCallback, + useEffect, + useState, +} from 'react'; + +import { translate as __ } from 'foremanReact/common/I18n'; +import { urlBuilder } from 'foremanReact/common/urlHelpers'; +import { STATUS } from 'foremanReact/constants'; +import { + lowerCase, + upperFirst, +} from 'lodash'; +import PropTypes from 'prop-types'; +import { + shallowEqual, + useDispatch, + useSelector, +} from 'react-redux'; import useDeepCompareEffect from 'use-deep-compare-effect'; -import { lowerCase, upperFirst } from 'lodash'; -import { useSelector, shallowEqual, useDispatch } from 'react-redux'; + import { - Bullseye, - Split, - SplitItem, - Button, ActionList, ActionListItem, + Bullseye, + Button, Dropdown, DropdownItem, KebabToggle, + Split, + SplitItem, + Checkbox, } 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 { + TableVariant, + Thead, + Tbody, + Tr, + Th, + Td, +} from '@patternfly/react-table'; +import AddedStatusLabel from '../../../../components/AddedStatusLabel'; +import SelectableDropdown from '../../../../components/SelectableDropdown'; import TableWrapper from '../../../../components/Table/TableWrapper'; -import onSelect from '../../../../components/Table/helpers'; -import { getContentViewRepositories, getRepositoryTypes, updateContentView } from '../ContentViewDetailActions'; import { + ADDED, + ALL_STATUSES, + NOT_ADDED, +} from '../../ContentViewsConstants'; +import { hasPermission } from '../../helpers'; +import { + getContentViewRepositories, + getRepositoryTypes, + updateContentView, +} from '../ContentViewDetailActions'; +import { selectCVRepos, - selectCVReposStatus, selectCVReposError, + selectCVReposStatus, selectRepoTypes, selectRepoTypesStatus, } from '../ContentViewDetailSelectors'; -import { ADDED, NOT_ADDED, ALL_STATUSES } from '../../ContentViewsConstants'; import ContentCounts from './ContentCounts'; import LastSync from './LastSync'; import RepoIcon from './RepoIcon'; -import AddedStatusLabel from '../../../../components/AddedStatusLabel'; -import SelectableDropdown from '../../../../components/SelectableDropdown'; -import { hasPermission } from '../../helpers'; +import { useSelectionSet } from '../../../../components/Table/TableHooks'; const allRepositories = 'All repositories'; // Add any exceptions to the display names here // [API_value]: displayed_value @@ -52,78 +80,50 @@ const { results, ...metadata } = response; 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 { permissions } = details; - - const [rows, setRows] = useState([]); - const deselectAll = () => setRows(rows.map(row => ({ ...row, selected: false }))); + const { permissions, generated_for: generatedFor, import_only: importOnly } = details; + const generatedContentView = generatedFor !== 'none'; 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 hasAddedSelected = rows.some(({ selected, added }) => selected && added); - const hasNotAddedSelected = rows.some(({ selected, added }) => selected && !added); + const { repository_ids: repositoryIds = [] } = details; + const { + isSelected, + selectOne, + selectNone, + selectedCount, + selectedResults, + selectionSet, + isSelectable, + ...selectAll + } = useSelectionSet({ + results, + metadata, + }); + + const hasAddedSelected = selectedResults.some(({ id }) => repositoryIds.includes(id)); + const hasNotAddedSelected = selectedResults.some(({ id }) => !repositoryIds.includes(id)); + const columnHeaders = [ - { title: __('Type'), transforms: [fitContent] }, + __('Type'), __('Name'), __('Product'), __('Sync state'), __('Content'), - { title: __('Status') }, + __('Status'), ]; - const loading = status === STATUS.PENDING; - const buildRows = useCallback(() => { - const newRows = []; - results.forEach((repo) => { - const { - id, - content_type: contentType, - name, - added_to_content_view: addedToCV, - product: { id: productId, name: productName }, - content_counts: counts, - last_sync_words: lastSyncWords, - last_sync: lastSync, - } = repo; - - const cells = [ - { title: <Bullseye><RepoIcon type={contentType} /></Bullseye> }, - { title: <a href={urlBuilder(`products/${productId}/repositories`, '', id)}>{name}</a> }, - productName, - { title: <LastSync {...{ startedAt: lastSync?.started_at, lastSyncWords, lastSync }} /> }, - { title: <ContentCounts {...{ counts, productId }} repoId={id} /> }, - { - title: <AddedStatusLabel added={addedToCV || statusSelected === ADDED} />, - }, - ]; - newRows.push({ - repoId: id, - cells, - added: addedToCV || statusSelected === ADDED, - }); - }); - return newRows; - }, [statusSelected, results]); - - useDeepCompareEffect(() => { - if (!loading && results) { - const newRows = buildRows(results); - setRows(newRows); - } - }, [response, loading, buildRows, results]); - useEffect(() => { dispatch(getRepositoryTypes()); }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Get repo type filter selections dynamically from the API useDeepCompareEffect(() => { if (repoTypesStatus === STATUS.RESOLVED && repoTypesResponse) { const allRepoTypes = {}; allRepoTypes[allRepositories] = 'all'; @@ -135,16 +135,16 @@ }); setRepoTypes(allRepoTypes); } }, [repoTypesResponse, repoTypesStatus]); + const toggleBulkAction = () => { setBulkActionOpen(!bulkActionOpen); }; const onAdd = (repos) => { - const { repository_ids: repositoryIds = [] } = details; dispatch(updateContentView( cvId, { repository_ids: repositoryIds.concat(repos) }, () => dispatch(getContentViewRepositories( @@ -157,11 +157,10 @@ )); }; 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 }, () => dispatch(getContentViewRepositories( @@ -174,53 +173,46 @@ )); }; const addBulk = () => { setBulkActionOpen(false); - const reposToAdd = rows.filter(({ selected, added }) => - selected && !added).map(({ repoId }) => repoId); - deselectAll(); + const reposToAdd = selectedResults.filter(selectedRepo => + !repositoryIds.includes(selectedRepo.id)).map(({ id }) => id); + selectNone(); onAdd(reposToAdd); }; const removeBulk = () => { setBulkActionOpen(false); - const reposToDelete = rows.filter(({ selected, added }) => - selected && added).map(({ repoId }) => repoId); - deselectAll(); + const reposToDelete = selectedResults.filter(selectedRepo => + repositoryIds.includes(selectedRepo.id)).map(({ id }) => id); + selectNone(); onRemove(reposToDelete); }; - const actionResolver = ({ - parent, - compoundParent, - noactions, - added, - }) => { - if (parent || compoundParent || noactions) return null; - return [ - { - title: 'Add', - isDisabled: added, - onClick: (_event, _rowId, rowInfo) => { - onAdd(rowInfo.repoId); - }, + const rowDropdownItems = ({ id }) => [ + { + title: 'Add', + ouiaId: `add-repository-${id}`, + isDisabled: importOnly || generatedContentView || repositoryIds.includes(id), + onClick: () => { + onAdd(id); }, - { - title: 'Remove', - isDisabled: !added, - onClick: (_event, _rowId, rowInfo) => { - onRemove(rowInfo.repoId); - }, + }, + { + title: 'Remove', + ouiaId: `remove-repository-${id}`, + isDisabled: importOnly || generatedContentView || !repositoryIds.includes(id), + onClick: () => { + onRemove(id); }, - ]; - }; + }, + ]; const getCVReposWithOptions = useCallback((params = {}) => { const allParams = { ...params }; if (typeSelected !== 'All repositories') allParams.content_type = repoTypes[typeSelected]; - return getContentViewRepositories(cvId, allParams, statusSelected); }, [cvId, repoTypes, statusSelected, typeSelected]); const emptyContentTitle = __("You currently don't have any repositories to add to this content view."); const emptyContentBody = __('Please add some repositories.'); // needs link @@ -228,19 +220,18 @@ const emptySearchBody = __('Try changing your search settings.'); const activeFilters = [typeSelected, statusSelected]; const defaultFilters = [allRepositories, ALL_STATUSES]; const dropdownItems = [ - <DropdownItem aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasAddedSelected} component="button" onClick={removeBulk}> + <DropdownItem ouiaId="bulk-remove-repositories" aria-label="bulk_remove" key="bulk_remove" isDisabled={!hasAddedSelected} component="button" onClick={removeBulk}> {__('Remove')} </DropdownItem>, ]; return ( <TableWrapper {...{ - rows, metadata, emptyContentTitle, emptyContentBody, emptySearchTitle, emptySearchBody, @@ -248,18 +239,20 @@ updateSearchQuery, error, status, activeFilters, defaultFilters, + selectedCount, + selectNone, }} - actionResolver={hasPermission(permissions, 'edit_content_views') ? actionResolver : null} - onSelect={hasPermission(permissions, 'edit_content_views') ? onSelect(rows, setRows) : null} - cells={columnHeaders} + ouiaId="content-view-repositories-table" + {...selectAll} variant={TableVariant.compact} autocompleteEndpoint="/repositories/auto_complete_search" fetchItems={useCallback(params => getCVReposWithOptions(params), [getCVReposWithOptions])} additionalListeners={[typeSelected, statusSelected]} + displaySelectAllCheckbox={hasPermission(permissions, 'edit_content_views')} actionButtons={ <Split hasGutter> <SplitItem> <SelectableDropdown items={Object.keys(repoTypes)} @@ -282,16 +275,17 @@ </SplitItem> {hasPermission(permissions, 'edit_content_views') && <SplitItem> <ActionList> <ActionListItem> - <Button onClick={addBulk} isDisabled={!hasNotAddedSelected} variant="primary" aria-label="add_repositories"> + <Button ouiaId="add-repositories" onClick={addBulk} isDisabled={!hasNotAddedSelected || importOnly || generatedContentView} variant="primary" aria-label="add_repositories"> {__('Add repositories')} </Button> </ActionListItem> <ActionListItem> <Dropdown + ouiaId="repositoies-bulk-actions" toggle={<KebabToggle aria-label="bulk_actions" onToggle={toggleBulkAction} />} isOpen={bulkActionOpen} isPlain dropdownItems={dropdownItems} /> @@ -299,18 +293,77 @@ </ActionList> </SplitItem> } </Split> } - /> + > + <Thead> + <Tr key="version-header"> + {hasPermission(permissions, 'edit_content_views') && <Th key="select-all" />} + {columnHeaders.map((title, index) => { + if (index === 0) { + return <Th modifier="fitContent" key={`col-header-${title}`}>{title}</Th>; + } + return <Th key={`col-header-${title}`}>{title}</Th>; + })} + </Tr> + </Thead> + <Tbody> + {results?.map((repo) => { + const { + id, + content_type: contentType, + name, + added_to_content_view: addedToCV, + product: { id: productId, name: productName }, + content_counts: counts, + last_sync_words: lastSyncWords, + last_sync: lastSync, + } = repo; + return ( + <Tr key={id} ouiaId={`repositories-table-row-${productName}-${name}`}> + {hasPermission(permissions, 'edit_content_views') && + <Td> + <Checkbox + id={id} + isChecked={isSelected(id)} + onChange={selected => + selectOne(selected, id, repo) + } + /> + </Td> + } + <Td><Bullseye><RepoIcon type={contentType} /></Bullseye></Td> + <Td> + <a href={urlBuilder(`products/${productId}/repositories`, '', id)}>{name}</a> + </Td> + <Td>{productName}</Td> + <Td> + <LastSync {...{ startedAt: lastSync?.started_at, lastSyncWords, lastSync }} /> + </Td> + <Td><ContentCounts {...{ counts, productId }} repoId={id} /></Td> + <Td><AddedStatusLabel added={addedToCV || statusSelected === ADDED} /></Td> + {hasPermission(permissions, 'edit_content_views') && + <Td + actions={{ + items: rowDropdownItems(repo), + }} + />} + </Tr> + ); + })} + </Tbody> + </TableWrapper> ); }; ContentViewRepositories.propTypes = { cvId: PropTypes.number.isRequired, details: PropTypes.shape({ repository_ids: PropTypes.arrayOf(PropTypes.number), permissions: PropTypes.shape({}), + import_only: PropTypes.bool, + generated_for: PropTypes.string, }).isRequired, }; export default ContentViewRepositories;