Merge pull request #5912 from keithjgrant/4239-pagination-on-delete

Adjust pagination when deleting final page of items

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-02-12 21:48:16 +00:00
committed by GitHub
6 changed files with 66 additions and 41 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { CredentialsAPI } from '@api'; import { CredentialsAPI } from '@api';
@@ -11,7 +11,12 @@ import PaginatedDataList, {
ToolbarAddButton, ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs'; import {
getQSConfig,
parseQueryString,
replaceParams,
encodeNonDefaultQueryString,
} from '@util/qs';
import { CredentialListItem } from '.'; import { CredentialListItem } from '.';
const QS_CONFIG = getQSConfig('credential', { const QS_CONFIG = getQSConfig('credential', {
@@ -30,6 +35,7 @@ function CredentialList({ i18n }) {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const location = useLocation(); const location = useLocation();
const history = useHistory();
const loadCredentials = async ({ search }) => { const loadCredentials = async ({ search }) => {
const params = parseQueryString(QS_CONFIG, search); const params = parseQueryString(QS_CONFIG, search);
@@ -92,20 +98,21 @@ function CredentialList({ i18n }) {
setDeletionError(error); setDeletionError(error);
} }
adjustPagination();
setSelected([]);
};
const adjustPagination = () => {
const params = parseQueryString(QS_CONFIG, location.search); const params = parseQueryString(QS_CONFIG, location.search);
try { if (params.page > 1 && selected.length === credentials.length) {
const { const newParams = encodeNonDefaultQueryString(
data: { count, results }, QS_CONFIG,
} = await CredentialsAPI.read(params); replaceParams(params, { page: params.page - 1 })
);
setCredentials(results); history.push(`${location.pathname}?${newParams}`);
setCredentialCount(count); } else {
setSelected([]); loadCredentials(location);
} catch (error) {
setContentError(error);
} }
setHasContentLoading(false);
}; };
const canAdd = const canAdd =

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom'; import { useLocation, useHistory, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
@@ -13,7 +13,12 @@ import PaginatedDataList, {
ToolbarAddButton, ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs'; import {
getQSConfig,
parseQueryString,
replaceParams,
encodeNonDefaultQueryString,
} from '@util/qs';
import OrganizationListItem from './OrganizationListItem'; import OrganizationListItem from './OrganizationListItem';
@@ -25,6 +30,7 @@ const QS_CONFIG = getQSConfig('organization', {
function OrganizationsList({ i18n }) { function OrganizationsList({ i18n }) {
const location = useLocation(); const location = useLocation();
const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
@@ -81,7 +87,21 @@ function OrganizationsList({ i18n }) {
const handleOrgDelete = async () => { const handleOrgDelete = async () => {
await deleteOrganizations(); await deleteOrganizations();
await fetchOrganizations(); await adjustPagination();
setSelected([]);
};
const adjustPagination = () => {
const params = parseQueryString(QS_CONFIG, location.search);
if (params.page > 1 && selected.length === organizations.length) {
const newParams = encodeNonDefaultQueryString(
QS_CONFIG,
replaceParams(params, { page: params.page - 1 })
);
history.push(`${location.pathname}?${newParams}`);
} else {
fetchOrganizations();
}
}; };
const hasContentLoading = isDeleteLoading || isOrgsLoading; const hasContentLoading = isDeleteLoading || isOrgsLoading;

View File

@@ -29,7 +29,7 @@ const QS_CONFIG = getQSConfig('template', {
type: 'job_template,workflow_job_template', type: 'job_template,workflow_job_template',
}); });
function TemplatesList({ i18n }) { function TemplateList({ i18n }) {
const { id: projectId } = useParams(); const { id: projectId } = useParams();
const { pathname, search } = useLocation(); const { pathname, search } = useLocation();
@@ -268,5 +268,4 @@ function TemplatesList({ i18n }) {
); );
} }
export { TemplatesList as _TemplatesList }; export default withI18n()(TemplateList);
export default withI18n()(TemplatesList);

View File

@@ -9,7 +9,7 @@ import {
} from '@api'; } from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import TemplatesList from './TemplateList'; import TemplateList from './TemplateList';
jest.mock('@api'); jest.mock('@api');
@@ -71,7 +71,7 @@ const mockTemplates = [
}, },
]; ];
describe('<TemplatesList />', () => { describe('<TemplateList />', () => {
beforeEach(() => { beforeEach(() => {
UnifiedJobTemplatesAPI.read.mockResolvedValue({ UnifiedJobTemplatesAPI.read.mockResolvedValue({
data: { data: {
@@ -94,7 +94,7 @@ describe('<TemplatesList />', () => {
test('initially renders successfully', async () => { test('initially renders successfully', async () => {
await act(async () => { await act(async () => {
mountWithContexts( mountWithContexts(
<TemplatesList <TemplateList
match={{ path: '/templates', url: '/templates' }} match={{ path: '/templates', url: '/templates' }}
location={{ search: '', pathname: '/templates' }} location={{ search: '', pathname: '/templates' }}
/> />
@@ -105,7 +105,7 @@ describe('<TemplatesList />', () => {
test('Templates are retrieved from the api and the components finishes loading', async () => { test('Templates are retrieved from the api and the components finishes loading', async () => {
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<TemplatesList />); wrapper = mountWithContexts(<TemplateList />);
}); });
expect(UnifiedJobTemplatesAPI.read).toBeCalled(); expect(UnifiedJobTemplatesAPI.read).toBeCalled();
await act(async () => { await act(async () => {
@@ -115,7 +115,7 @@ describe('<TemplatesList />', () => {
}); });
test('handleSelect is called when a template list item is selected', async () => { test('handleSelect is called when a template list item is selected', async () => {
const wrapper = mountWithContexts(<TemplatesList />); const wrapper = mountWithContexts(<TemplateList />);
await act(async () => { await act(async () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
@@ -143,7 +143,7 @@ describe('<TemplatesList />', () => {
}); });
test('handleSelectAll is called when a template list item is selected', async () => { test('handleSelectAll is called when a template list item is selected', async () => {
const wrapper = mountWithContexts(<TemplatesList />); const wrapper = mountWithContexts(<TemplateList />);
await act(async () => { await act(async () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
@@ -158,7 +158,7 @@ describe('<TemplatesList />', () => {
}); });
test('delete button is disabled if user does not have delete capabilities on a selected template', async () => { test('delete button is disabled if user does not have delete capabilities on a selected template', async () => {
const wrapper = mountWithContexts(<TemplatesList />); const wrapper = mountWithContexts(<TemplateList />);
await act(async () => { await act(async () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
@@ -217,7 +217,7 @@ describe('<TemplatesList />', () => {
}); });
test('api is called to delete templates for each selected template.', async () => { test('api is called to delete templates for each selected template.', async () => {
const wrapper = mountWithContexts(<TemplatesList />); const wrapper = mountWithContexts(<TemplateList />);
await act(async () => { await act(async () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
@@ -277,7 +277,7 @@ describe('<TemplatesList />', () => {
}, },
}) })
); );
const wrapper = mountWithContexts(<TemplatesList />); const wrapper = mountWithContexts(<TemplateList />);
await act(async () => { await act(async () => {
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
@@ -315,7 +315,7 @@ describe('<TemplatesList />', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<Route <Route
path="/projects/:id/job_templates" path="/projects/:id/job_templates"
component={() => <TemplatesList />} component={() => <TemplateList />}
/>, />,
{ {
context: { context: {

View File

@@ -2,12 +2,12 @@ import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import TemplatesListItem from './TemplateListItem'; import TemplateListItem from './TemplateListItem';
describe('<TemplatesListItem />', () => { describe('<TemplateListItem />', () => {
test('launch button shown to users with start capabilities', () => { test('launch button shown to users with start capabilities', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -26,7 +26,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('launch button hidden from users without start capabilities', () => { test('launch button hidden from users without start capabilities', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -45,7 +45,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('edit button shown to users with edit capabilities', () => { test('edit button shown to users with edit capabilities', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -64,7 +64,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('edit button hidden from users without edit capabilities', () => { test('edit button hidden from users without edit capabilities', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -83,7 +83,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('missing resource icon is shown.', () => { test('missing resource icon is shown.', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -102,7 +102,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('missing resource icon is not shown when there is a project and an inventory.', () => { test('missing resource icon is not shown when there is a project and an inventory.', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -123,7 +123,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => { test('missing resource icon is not shown when inventory is prompt_on_launch, and a project', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,
@@ -144,7 +144,7 @@ describe('<TemplatesListItem />', () => {
}); });
test('missing resource icon is not shown type is workflow_job_template', () => { test('missing resource icon is not shown type is workflow_job_template', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<TemplatesListItem <TemplateListItem
isSelected={false} isSelected={false}
template={{ template={{
id: 1, id: 1,

View File

@@ -90,7 +90,6 @@ export { addDefaultsToObject as _addDefaultsToObject };
/** /**
* Convert query param object to url query string * Convert query param object to url query string
* Used to encode params for interacting with the api * Used to encode params for interacting with the api
* @param {object} qs config object for namespacing params, filtering defaults
* @param {object} query param object * @param {object} query param object
* @return {string} url query string * @return {string} url query string
*/ */