updates based on pr feedback

run prettier
update hasContentError to contentError in all the places
function naming updates
This commit is contained in:
John Mitchell 2019-07-26 10:03:46 -04:00
parent 357887417c
commit bdfeb2cb9c
31 changed files with 929 additions and 664 deletions

View File

@ -1,48 +1,46 @@
import axios from 'axios';
import {
encodeQueryString
} from '@util/qs';
import { encodeQueryString } from '@util/qs';
const defaultHttp = axios.create({
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken',
paramsSerializer(params) {
return encodeQueryString(params);
}
},
});
class Base {
constructor (http = defaultHttp, baseURL) {
constructor(http = defaultHttp, baseURL) {
this.http = http;
this.baseUrl = baseURL;
}
create (data) {
create(data) {
return this.http.post(this.baseUrl, data);
}
destroy (id) {
destroy(id) {
return this.http.delete(`${this.baseUrl}${id}/`);
}
read (params) {
read(params) {
return this.http.get(this.baseUrl, { params });
}
readDetail (id) {
readDetail(id) {
return this.http.get(`${this.baseUrl}${id}/`);
}
readOptions () {
readOptions() {
return this.http.options(this.baseUrl);
}
replace (id, data) {
replace(id, data) {
return this.http.put(`${this.baseUrl}${id}/`, data);
}
update (id, data) {
update(id, data) {
return this.http.patch(`${this.baseUrl}${id}/`, data);
}
}

View File

@ -3,14 +3,14 @@ import Base from './Base';
describe('Base', () => {
const createPromise = () => Promise.resolve();
const mockBaseURL = '/api/v2/organizations/';
const mockHttp = ({
const mockHttp = {
delete: jest.fn(createPromise),
get: jest.fn(createPromise),
options: jest.fn(createPromise),
patch: jest.fn(createPromise),
post: jest.fn(createPromise),
put: jest.fn(createPromise)
});
put: jest.fn(createPromise),
};
const BaseAPI = new Base(mockHttp, mockBaseURL);
@ -18,7 +18,7 @@ describe('Base', () => {
jest.clearAllMocks();
});
test('create calls http method with expected data', async (done) => {
test('create calls http method with expected data', async done => {
const data = { name: 'test ' };
await BaseAPI.create(data);
@ -28,19 +28,21 @@ describe('Base', () => {
done();
});
test('destroy calls http method with expected data', async (done) => {
test('destroy calls http method with expected data', async done => {
const resourceId = 1;
await BaseAPI.destroy(resourceId);
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
expect(mockHttp.delete.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.delete.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
done();
});
test('read calls http method with expected data', async (done) => {
test('read calls http method with expected data', async done => {
const testParams = { foo: 'bar' };
const testParamsDuplicates = { foo: ['bar', 'baz']};
const testParamsDuplicates = { foo: ['bar', 'baz'] };
await BaseAPI.read(testParams);
await BaseAPI.read();
@ -48,25 +50,29 @@ describe('Base', () => {
expect(mockHttp.get).toHaveBeenCalledTimes(3);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": {"foo": "bar"}});
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: { foo: 'bar' } });
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": undefined});
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: undefined });
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}});
expect(mockHttp.get.mock.calls[2][1]).toEqual({
params: { foo: ['bar', 'baz'] },
});
done();
});
test('readDetail calls http method with expected data', async (done) => {
test('readDetail calls http method with expected data', async done => {
const resourceId = 1;
await BaseAPI.readDetail(resourceId);
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.get.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
done();
});
test('readOptions calls http method with expected data', async (done) => {
test('readOptions calls http method with expected data', async done => {
await BaseAPI.readOptions();
expect(mockHttp.options).toHaveBeenCalledTimes(1);
@ -74,27 +80,31 @@ describe('Base', () => {
done();
});
test('replace calls http method with expected data', async (done) => {
test('replace calls http method with expected data', async done => {
const resourceId = 1;
const data = { name: 'test ' };
await BaseAPI.replace(resourceId, data);
expect(mockHttp.put).toHaveBeenCalledTimes(1);
expect(mockHttp.put.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.put.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
done();
});
test('update calls http method with expected data', async (done) => {
test('update calls http method with expected data', async done => {
const resourceId = 1;
const data = { name: 'test ' };
await BaseAPI.update(resourceId, data);
expect(mockHttp.patch).toHaveBeenCalledTimes(1);
expect(mockHttp.patch.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.patch.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
done();

View File

@ -1,15 +1,24 @@
const InstanceGroupsMixin = (parent) => class extends parent {
readInstanceGroups (resourceId, params) {
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, params);
}
const InstanceGroupsMixin = parent =>
class extends parent {
readInstanceGroups(resourceId, params) {
return this.http.get(
`${this.baseUrl}${resourceId}/instance_groups/`,
params
);
}
associateInstanceGroup (resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId });
}
associateInstanceGroup(resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
id: instanceGroupId,
});
}
disassociateInstanceGroup (resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true });
}
};
disassociateInstanceGroup(resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
id: instanceGroupId,
disassociate: true,
});
}
};
export default InstanceGroupsMixin;

View File

@ -1,65 +1,106 @@
const NotificationsMixin = (parent) => class extends parent {
readOptionsNotificationTemplates(id) {
return this.http.options(`${this.baseUrl}${id}/notification_templates/`);
}
readNotificationTemplates (id, params) {
return this.http.get(`${this.baseUrl}${id}/notification_templates/`, params);
}
readNotificationTemplatesSuccess (id, params) {
return this.http.get(`${this.baseUrl}${id}/notification_templates_success/`, params);
}
readNotificationTemplatesError (id, params) {
return this.http.get(`${this.baseUrl}${id}/notification_templates_error/`, params);
}
associateNotificationTemplatesSuccess (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId });
}
disassociateNotificationTemplatesSuccess (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_success/`, { id: notificationId, disassociate: true });
}
associateNotificationTemplatesError (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId });
}
disassociateNotificationTemplatesError (resourceId, notificationId) {
return this.http.post(`${this.baseUrl}${resourceId}/notification_templates_error/`, { id: notificationId, disassociate: true });
}
/**
* This is a helper method meant to simplify setting the "on" or "off" status of
* a related notification.
*
* @param[resourceId] - id of the base resource
* @param[notificationId] - id of the notification
* @param[notificationType] - the type of notification, options are "success" and "error"
* @param[associationState] - Boolean for associating or disassociating, options are true or false
*/
// eslint-disable-next-line max-len
updateNotificationTemplateAssociation (resourceId, notificationId, notificationType, associationState) {
if (notificationType === 'success' && associationState === true) {
return this.associateNotificationTemplatesSuccess(resourceId, notificationId);
const NotificationsMixin = parent =>
class extends parent {
readOptionsNotificationTemplates(id) {
return this.http.options(`${this.baseUrl}${id}/notification_templates/`);
}
if (notificationType === 'success' && associationState === false) {
return this.disassociateNotificationTemplatesSuccess(resourceId, notificationId);
readNotificationTemplates(id, params) {
return this.http.get(
`${this.baseUrl}${id}/notification_templates/`,
params
);
}
if (notificationType === 'error' && associationState === true) {
return this.associateNotificationTemplatesError(resourceId, notificationId);
readNotificationTemplatesSuccess(id, params) {
return this.http.get(
`${this.baseUrl}${id}/notification_templates_success/`,
params
);
}
if (notificationType === 'error' && associationState === false) {
return this.disassociateNotificationTemplatesError(resourceId, notificationId);
readNotificationTemplatesError(id, params) {
return this.http.get(
`${this.baseUrl}${id}/notification_templates_error/`,
params
);
}
throw new Error(`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`);
}
};
associateNotificationTemplatesSuccess(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_success/`,
{ id: notificationId }
);
}
disassociateNotificationTemplatesSuccess(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_success/`,
{ id: notificationId, disassociate: true }
);
}
associateNotificationTemplatesError(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_error/`,
{ id: notificationId }
);
}
disassociateNotificationTemplatesError(resourceId, notificationId) {
return this.http.post(
`${this.baseUrl}${resourceId}/notification_templates_error/`,
{ id: notificationId, disassociate: true }
);
}
/**
* This is a helper method meant to simplify setting the "on" or "off" status of
* a related notification.
*
* @param[resourceId] - id of the base resource
* @param[notificationId] - id of the notification
* @param[notificationType] - the type of notification, options are "success" and "error"
* @param[associationState] - Boolean for associating or disassociating, options are true or false
*/
// eslint-disable-next-line max-len
updateNotificationTemplateAssociation(
resourceId,
notificationId,
notificationType,
associationState
) {
if (notificationType === 'success' && associationState === true) {
return this.associateNotificationTemplatesSuccess(
resourceId,
notificationId
);
}
if (notificationType === 'success' && associationState === false) {
return this.disassociateNotificationTemplatesSuccess(
resourceId,
notificationId
);
}
if (notificationType === 'error' && associationState === true) {
return this.associateNotificationTemplatesError(
resourceId,
notificationId
);
}
if (notificationType === 'error' && associationState === false) {
return this.disassociateNotificationTemplatesError(
resourceId,
notificationId
);
}
throw new Error(
`Unsupported notificationType, associationState combination: ${notificationType}, ${associationState}`
);
}
};
export default NotificationsMixin;

View File

@ -3,16 +3,16 @@ import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/organizations/';
}
readAccessList (id, params) {
readAccessList(id, params) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
}
readTeams (id, params) {
readTeams(id, params) {
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
}
}

View File

@ -4,7 +4,7 @@ import { describeNotificationMixin } from '../../../testUtils/apiReusable';
describe('OrganizationsAPI', () => {
const orgId = 1;
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const mockHttp = { get: jest.fn(createPromise) };
const OrganizationsAPI = new Organizations(mockHttp);
@ -12,9 +12,9 @@ describe('OrganizationsAPI', () => {
jest.clearAllMocks();
});
test('read access list calls get with expected params', async (done) => {
test('read access list calls get with expected params', async done => {
const testParams = { foo: 'bar' };
const testParamsDuplicates = { foo: ['bar', 'baz']};
const testParamsDuplicates = { foo: ['bar', 'baz'] };
const mockBaseURL = `/api/v2/organizations/${orgId}/access_list/`;
@ -24,17 +24,19 @@ describe('OrganizationsAPI', () => {
expect(mockHttp.get).toHaveBeenCalledTimes(3);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": undefined});
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined });
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": {"foo": "bar"}});
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } });
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}});
expect(mockHttp.get.mock.calls[2][1]).toEqual({
params: { foo: ['bar', 'baz'] },
});
done();
});
test('read teams calls get with expected params', async (done) => {
test('read teams calls get with expected params', async done => {
const testParams = { foo: 'bar' };
const testParamsDuplicates = { foo: ['bar', 'baz']};
const testParamsDuplicates = { foo: ['bar', 'baz'] };
const mockBaseURL = `/api/v2/organizations/${orgId}/teams/`;
@ -44,11 +46,13 @@ describe('OrganizationsAPI', () => {
expect(mockHttp.get).toHaveBeenCalledTimes(3);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[0][1]).toEqual({"params": undefined});
expect(mockHttp.get.mock.calls[0][1]).toEqual({ params: undefined });
expect(mockHttp.get.mock.calls[1][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[1][1]).toEqual({"params": {"foo": "bar"}});
expect(mockHttp.get.mock.calls[1][1]).toEqual({ params: { foo: 'bar' } });
expect(mockHttp.get.mock.calls[2][0]).toEqual(`${mockBaseURL}`);
expect(mockHttp.get.mock.calls[2][1]).toEqual({"params": {"foo": ["bar", "baz"]}});
expect(mockHttp.get.mock.calls[2][1]).toEqual({
params: { foo: ['bar', 'baz'] },
});
done();
});
});

View File

@ -8,14 +8,13 @@ import SelectRoleStep from './SelectRoleStep';
import SelectableCard from './SelectableCard';
import { TeamsAPI, UsersAPI } from '../../api';
const readUsers = async (queryParams) => UsersAPI.read(
Object.assign(queryParams, { is_superuser: false })
);
const readUsers = async queryParams =>
UsersAPI.read(Object.assign(queryParams, { is_superuser: false }));
const readTeams = async (queryParams) => TeamsAPI.read(queryParams);
const readTeams = async queryParams => TeamsAPI.read(queryParams);
class AddResourceRole extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -25,67 +24,69 @@ class AddResourceRole extends React.Component {
currentStepId: 1,
};
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this);
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(
this
);
this.handleResourceSelect = this.handleResourceSelect.bind(this);
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
this.handleWizardNext = this.handleWizardNext.bind(this);
this.handleWizardSave = this.handleWizardSave.bind(this);
}
handleResourceCheckboxClick (user) {
handleResourceCheckboxClick(user) {
const { selectedResourceRows } = this.state;
const selectedIndex = selectedResourceRows
.findIndex(selectedRow => selectedRow.id === user.id);
const selectedIndex = selectedResourceRows.findIndex(
selectedRow => selectedRow.id === user.id
);
if (selectedIndex > -1) {
selectedResourceRows.splice(selectedIndex, 1);
this.setState({ selectedResourceRows });
} else {
this.setState(prevState => ({
selectedResourceRows: [...prevState.selectedResourceRows, user]
selectedResourceRows: [...prevState.selectedResourceRows, user],
}));
}
}
handleRoleCheckboxClick (role) {
handleRoleCheckboxClick(role) {
const { selectedRoleRows } = this.state;
const selectedIndex = selectedRoleRows
.findIndex(selectedRow => selectedRow.id === role.id);
const selectedIndex = selectedRoleRows.findIndex(
selectedRow => selectedRow.id === role.id
);
if (selectedIndex > -1) {
selectedRoleRows.splice(selectedIndex, 1);
this.setState({ selectedRoleRows });
} else {
this.setState(prevState => ({
selectedRoleRows: [...prevState.selectedRoleRows, role]
selectedRoleRows: [...prevState.selectedRoleRows, role],
}));
}
}
handleResourceSelect (resourceType) {
handleResourceSelect(resourceType) {
this.setState({
selectedResource: resourceType,
selectedResourceRows: [],
selectedRoleRows: []
selectedRoleRows: [],
});
}
handleWizardNext (step) {
handleWizardNext(step) {
this.setState({
currentStepId: step.id,
});
}
async handleWizardSave () {
const {
onSave
} = this.props;
async handleWizardSave() {
const { onSave } = this.props;
const {
selectedResourceRows,
selectedRoleRows,
selectedResource
selectedResource,
} = this.state;
try {
@ -95,11 +96,17 @@ class AddResourceRole extends React.Component {
for (let j = 0; j < selectedRoleRows.length; j++) {
if (selectedResource === 'users') {
roleRequests.push(
UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
UsersAPI.associateRole(
selectedResourceRows[i].id,
selectedRoleRows[j].id
)
);
} else if (selectedResource === 'teams') {
roleRequests.push(
TeamsAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
TeamsAPI.associateRole(
selectedResourceRows[i].id,
selectedRoleRows[j].id
)
);
}
}
@ -112,25 +119,31 @@ class AddResourceRole extends React.Component {
}
}
render () {
render() {
const {
selectedResource,
selectedResourceRows,
selectedRoleRows,
currentStepId,
} = this.state;
const {
onClose,
roles,
i18n
} = this.props;
const { onClose, roles, i18n } = this.props;
const userColumns = [
{ name: i18n._(t`Username`), key: 'username', isSortable: true, isSearchable: true }
{
name: i18n._(t`Username`),
key: 'username',
isSortable: true,
isSearchable: true,
},
];
const teamColumns = [
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
];
let wizardTitle = '';
@ -164,7 +177,7 @@ class AddResourceRole extends React.Component {
/>
</div>
),
enableNext: selectedResource !== null
enableNext: selectedResource !== null,
},
{
id: 2,
@ -195,7 +208,7 @@ class AddResourceRole extends React.Component {
)}
</Fragment>
),
enableNext: selectedResourceRows.length > 0
enableNext: selectedResourceRows.length > 0,
},
{
id: 3,
@ -211,8 +224,8 @@ class AddResourceRole extends React.Component {
/>
),
nextButtonText: i18n._(t`Save`),
enableNext: selectedRoleRows.length > 0
}
enableNext: selectedRoleRows.length > 0,
},
];
const currentStep = steps.find(step => step.id === currentStepId);
@ -236,11 +249,11 @@ class AddResourceRole extends React.Component {
AddResourceRole.propTypes = {
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
roles: PropTypes.shape()
roles: PropTypes.shape(),
};
AddResourceRole.defaultProps = {
roles: {}
roles: {},
};
export { AddResourceRole as _AddResourceRole };

View File

@ -40,10 +40,7 @@ class SelectResourceStep extends React.Component {
async readResourceList() {
const { onSearch, location } = this.props;
const queryParams = parseQueryString(
this.qsConfig,
location.search
);
const queryParams = parseQueryString(this.qsConfig, location.search);
this.setState({
isLoading: true,

View File

@ -7,7 +7,7 @@ import SelectResourceStep from './SelectResourceStep';
describe('<SelectResourceStep />', () => {
const columns = [
{ name: 'Username', key: 'username', isSortable: true, isSearchable: true }
{ name: 'Username', key: 'username', isSortable: true, isSearchable: true },
];
afterEach(() => {
jest.restoreAllMocks();
@ -30,9 +30,9 @@ describe('<SelectResourceStep />', () => {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
]
}
{ id: 2, username: 'bar', url: 'item/2' },
],
},
});
mountWithContexts(
<SelectResourceStep
@ -46,25 +46,25 @@ describe('<SelectResourceStep />', () => {
expect(handleSearch).toHaveBeenCalledWith({
order_by: 'username',
page: 1,
page_size: 5
page_size: 5,
});
});
test('readResourceList properly adds rows to state', async () => {
const selectedResourceRows = [
{ id: 1, username: 'foo', url: 'item/1' }
];
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
const handleSearch = jest.fn().mockResolvedValue({
data: {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
]
}
{ id: 2, username: 'bar', url: 'item/2' },
],
},
});
const history = createMemoryHistory({
initialEntries: ['/organizations/1/access?resource.page=1&resource.order_by=-username'],
initialEntries: [
'/organizations/1/access?resource.page=1&resource.order_by=-username',
],
});
const wrapper = await mountWithContexts(
<SelectResourceStep
@ -74,7 +74,10 @@ describe('<SelectResourceStep />', () => {
onSearch={handleSearch}
selectedResourceRows={selectedResourceRows}
sortedColumnKey="username"
/>, { context: { router: { history, route: { location: history.location } } } }
/>,
{
context: { router: { history, route: { location: history.location } } },
}
).find('SelectResourceStep');
await wrapper.instance().readResourceList();
expect(handleSearch).toHaveBeenCalledWith({
@ -84,7 +87,7 @@ describe('<SelectResourceStep />', () => {
});
expect(wrapper.state('resources')).toEqual([
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
{ id: 2, username: 'bar', url: 'item/2' },
]);
});
@ -94,8 +97,8 @@ describe('<SelectResourceStep />', () => {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
]
{ id: 2, username: 'bar', url: 'item/2' },
],
};
const wrapper = mountWithContexts(
<SelectResourceStep
@ -111,7 +114,9 @@ describe('<SelectResourceStep />', () => {
wrapper.update();
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
expect(checkboxListItemWrapper.length).toBe(2);
checkboxListItemWrapper.first().find('input[type="checkbox"]')
checkboxListItemWrapper
.first()
.find('input[type="checkbox"]')
.simulate('change', { target: { checked: true } });
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
});

View File

@ -3,14 +3,21 @@ import { number, bool } from 'prop-types';
import styled from 'styled-components';
import Chip from './Chip';
const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...props }) => {
const ChipGroup = ({
children,
className,
showOverflowAfter,
displayAll,
...props
}) => {
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
const toggleIsOpen = () => setIsExpanded(!isExpanded);
const mappedChildren = React.Children.map(children, c => (
const mappedChildren = React.Children.map(children, c =>
React.cloneElement(c, { component: 'li' })
));
const showOverflowToggle = showOverflowAfter && children.length > showOverflowAfter;
);
const showOverflowToggle =
showOverflowAfter && children.length > showOverflowAfter;
const numToShow = isExpanded
? children.length
: Math.min(showOverflowAfter, children.length);
@ -30,17 +37,17 @@ const ChipGroup = ({ children, className, showOverflowAfter, displayAll, ...prop
};
ChipGroup.propTypes = {
showOverflowAfter: number,
displayAll: bool
displayAll: bool,
};
ChipGroup.defaultProps = {
showOverflowAfter: null,
displayAll: false
displayAll: false,
};
export default styled(ChipGroup)`
--pf-c-chip-group--c-chip--MarginRight: 10px;
--pf-c-chip-group--c-chip--MarginBottom: 10px;
> .pf-c-chip.pf-m-overflow button {
padding: 3px 8px;
}

View File

@ -13,7 +13,9 @@ describe('<DataListToolbar />', () => {
});
test('it triggers the expected callbacks', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
];
const search = 'button[aria-label="Search submit button"]';
const searchTextInput = 'input[aria-label="Search text input"]';
@ -59,14 +61,16 @@ describe('<DataListToolbar />', () => {
test('dropdown items sortable/searchable columns work', () => {
const sortDropdownToggleSelector = 'button[id="awx-sort"]';
const searchDropdownToggleSelector = 'button[id="awx-search"]';
const sortDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-sort"]';
const searchDropdownMenuItems = 'DropdownMenu > ul[aria-labelledby="awx-search"]';
const sortDropdownMenuItems =
'DropdownMenu > ul[aria-labelledby="awx-sort"]';
const searchDropdownMenuItems =
'DropdownMenu > ul[aria-labelledby="awx-search"]';
const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true, isSearchable: true },
{ name: 'Bar', key: 'bar', isSortable: true, isSearchable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -103,12 +107,16 @@ describe('<DataListToolbar />', () => {
);
toolbar.update();
const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector);
const sortDropdownToggleDescending = toolbar.find(
sortDropdownToggleSelector
);
expect(sortDropdownToggleDescending.length).toBe(1);
sortDropdownToggleDescending.simulate('click');
toolbar.update();
const sortDropdownItemsDescending = toolbar.find(sortDropdownMenuItems).children();
const sortDropdownItemsDescending = toolbar
.find(sortDropdownMenuItems)
.children();
expect(sortDropdownItemsDescending.length).toBe(2);
sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
@ -134,8 +142,12 @@ describe('<DataListToolbar />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
const numericColumns = [
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
];
toolbar = mountWithContexts(
<DataListToolbar
@ -183,7 +195,9 @@ describe('<DataListToolbar />', () => {
});
test('should render additionalControls', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
];
const onSearch = jest.fn();
const onSort = jest.fn();
const onSelectAll = jest.fn();
@ -194,7 +208,11 @@ describe('<DataListToolbar />', () => {
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
additionalControls={[<button key="1" id="test" type="button">click</button>]}
additionalControls={[
<button key="1" id="test" type="button">
click
</button>,
]}
/>
);

View File

@ -9,19 +9,19 @@ import { ChipGroup, Chip } from '@components/Chip';
import VerticalSeparator from '@components/VerticalSeparator';
const FilterTagsRow = styled.div`
display: flex;
padding: 15px 20px;
border-top: 1px solid #d2d2d2;
font-size: 14px;
align-items: center;
display: flex;
padding: 15px 20px;
border-top: 1px solid #d2d2d2;
font-size: 14px;
align-items: center;
`;
const ResultCount = styled.span`
font-weight: bold;
font-weight: bold;
`;
const FilterLabel = styled.span`
padding-right: 20px;
padding-right: 20px;
`;
// remove non-default query params so they don't show up as filter tags
@ -30,50 +30,59 @@ const filterDefaultParams = (paramsArr, config) => {
return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
};
const FilterTags = ({ i18n, itemCount, qsConfig, location, onRemove, onRemoveAll }) => {
const FilterTags = ({
i18n,
itemCount,
qsConfig,
location,
onRemove,
onRemoveAll,
}) => {
const queryParams = parseQueryString(qsConfig, location.search);
const queryParamsArr = [];
const displayAll = true;
const nonDefaultParams = filterDefaultParams(Object.keys(queryParams), qsConfig);
nonDefaultParams
.forEach(key => {
if (Array.isArray(queryParams[key])) {
queryParams[key].forEach(val => queryParamsArr.push({ key, value: val }));
} else {
queryParamsArr.push({ key, value: queryParams[key] });
}
});
const nonDefaultParams = filterDefaultParams(
Object.keys(queryParams),
qsConfig
);
nonDefaultParams.forEach(key => {
if (Array.isArray(queryParams[key])) {
queryParams[key].forEach(val => queryParamsArr.push({ key, value: val }));
} else {
queryParamsArr.push({ key, value: queryParams[key] });
}
});
return (queryParamsArr.length > 0) && (
<FilterTagsRow>
<ResultCount>
{`${itemCount} results`}
</ResultCount>
<VerticalSeparator />
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
<ChipGroup displayAll={displayAll}>
{queryParamsArr.map(({ key, value }) => (
<Chip
className="searchTagChip"
key={`${key}__${value}`}
isReadOnly={false}
onClick={() => onRemove(key, value)}
>
{value}
</Chip>
))}
<div className="pf-c-chip pf-m-overflow">
<Button
variant="plain"
type="button"
aria-label={i18n._(t`Clear all search filters`)}
onClick={onRemoveAll}
>
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
</Button>
</div>
</ChipGroup>
</FilterTagsRow>
return (
queryParamsArr.length > 0 && (
<FilterTagsRow>
<ResultCount>{`${itemCount} results`}</ResultCount>
<VerticalSeparator />
<FilterLabel>{i18n._(t`Active Filters:`)}</FilterLabel>
<ChipGroup displayAll={displayAll}>
{queryParamsArr.map(({ key, value }) => (
<Chip
className="searchTagChip"
key={`${key}__${value}`}
isReadOnly={false}
onClick={() => onRemove(key, value)}
>
{value}
</Chip>
))}
<div className="pf-c-chip pf-m-overflow">
<Button
variant="plain"
type="button"
aria-label={i18n._(t`Clear all search filters`)}
onClick={onRemoveAll}
>
<span className="pf-c-chip__text">{i18n._(t`Clear all`)}</span>
</Button>
</div>
</ChipGroup>
</FilterTagsRow>
)
);
};

View File

@ -26,14 +26,19 @@ describe('<ExpandCollapse />', () => {
test('renders non-default param tags based on location history', () => {
const history = createMemoryHistory({
initialEntries: ['/foo?item.page=1&item.page_size=2&item.foo=bar&item.baz=bust'],
initialEntries: [
'/foo?item.page=1&item.page_size=2&item.foo=bar&item.baz=bust',
],
});
const wrapper = mountWithContexts(
<FilterTags
qsConfig={qsConfig}
onRemove={onRemoveFn}
onRemoveAll={onRemoveAllFn}
/>, { context: { router: { history, route: { location: history.location } } } }
/>,
{
context: { router: { history, route: { location: history.location } } },
}
);
const chips = wrapper.find('.pf-c-chip.searchTagChip');
expect(chips.length).toBe(2);

View File

@ -10,7 +10,7 @@ import {
encodeNonDefaultQueryString,
parseQueryString,
addParams,
removeParams
removeParams,
} from '@util/qs';
import { QSConfig } from '@types';
@ -21,12 +21,12 @@ const EmptyStateControlsWrapper = styled.div`
margin-bottom: 20px;
justify-content: flex-end;
& > :not(:first-child) {
& > :not(:first-child) {
margin-left: 20px;
}
`;
class ListHeader extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
@ -35,7 +35,7 @@ class ListHeader extends React.Component {
this.handleRemoveAll = this.handleRemoveAll.bind(this);
}
getSortOrder () {
getSortOrder() {
const { qsConfig, location } = this.props;
const queryParams = parseQueryString(qsConfig, location.search);
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
@ -44,45 +44,47 @@ class ListHeader extends React.Component {
return [queryParams.order_by, 'ascending'];
}
handleSearch (key, value) {
handleSearch(key, value) {
const { history, qsConfig } = this.props;
const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { [key]: value }));
}
handleRemove (key, value) {
handleRemove(key, value) {
const { history, qsConfig } = this.props;
const { search } = history.location;
this.pushHistoryState(removeParams(qsConfig, search, { [key]: value }));
}
handleRemoveAll () {
handleRemoveAll() {
this.pushHistoryState(null);
}
handleSort (key, order) {
handleSort(key, order) {
const { history, qsConfig } = this.props;
const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, {
order_by: order === 'ascending' ? key : `-${key}`,
page: null,
}));
this.pushHistoryState(
addParams(qsConfig, search, {
order_by: order === 'ascending' ? key : `-${key}`,
page: null,
})
);
}
pushHistoryState (params) {
pushHistoryState(params) {
const { history, qsConfig } = this.props;
const { pathname } = history.location;
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
}
render () {
render() {
const {
emptyStateControls,
itemCount,
columns,
renderToolbar,
qsConfig
qsConfig,
} = this.props;
const [orderBy, sortOrder] = this.getSortOrder();
return (
@ -124,17 +126,19 @@ class ListHeader extends React.Component {
ListHeader.propTypes = {
itemCount: PropTypes.number.isRequired,
qsConfig: QSConfig.isRequired,
columns: arrayOf(shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
isSearchable: bool
})).isRequired,
columns: arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
isSearchable: bool,
})
).isRequired,
renderToolbar: PropTypes.func,
};
ListHeader.defaultProps = {
renderToolbar: (props) => (<DataListToolbar {...props} />),
renderToolbar: props => <DataListToolbar {...props} />,
};
export default withRouter(ListHeader);

View File

@ -17,7 +17,9 @@ describe('ListHeader', () => {
<ListHeader
itemCount={50}
qsConfig={qsConfig}
columns={[{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true }]}
columns={[
{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true },
]}
renderToolbar={renderToolbarFn}
/>
);
@ -33,8 +35,11 @@ describe('ListHeader', () => {
<ListHeader
itemCount={7}
qsConfig={qsConfig}
columns={[{ name: 'name', key: 'name', isSearchable: true, isSortable: true }]}
/>, { context: { router: { history } } }
columns={[
{ name: 'name', key: 'name', isSearchable: true, isSortable: true },
]}
/>,
{ context: { router: { history } } }
);
const toolbar = wrapper.find('DataListToolbar');

View File

@ -15,7 +15,7 @@ import DataListToolbar from '@components/DataListToolbar';
import {
encodeNonDefaultQueryString,
parseQueryString,
addParams
addParams,
} from '@util/qs';
import { pluralize, ucFirst } from '@util/strings';
@ -24,32 +24,32 @@ import { QSConfig } from '@types';
import PaginatedDataListItem from './PaginatedDataListItem';
class PaginatedDataList extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.handleSetPage = this.handleSetPage.bind(this);
this.handleSetPageSize = this.handleSetPageSize.bind(this);
}
handleSetPage (event, pageNumber) {
handleSetPage(event, pageNumber) {
const { history, qsConfig } = this.props;
const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { page: pageNumber }));
}
handleSetPageSize (event, pageSize) {
handleSetPageSize(event, pageSize) {
const { history, qsConfig } = this.props;
const { search } = history.location;
this.pushHistoryState(addParams(qsConfig, search, { page_size: pageSize }));
}
pushHistoryState (params) {
pushHistoryState(params) {
const { history, qsConfig } = this.props;
const { pathname } = history.location;
const encodedParams = encodeNonDefaultQueryString(qsConfig, params);
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
}
render () {
render() {
const {
contentError,
hasContentLoading,
@ -66,25 +66,42 @@ class PaginatedDataList extends React.Component {
i18n,
renderToolbar,
} = this.props;
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true }];
const columns = toolbarColumns.length
? toolbarColumns
: [
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
];
const queryParams = parseQueryString(qsConfig, location.search);
const itemDisplayName = ucFirst(pluralize(itemName));
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
const itemDisplayNamePlural = ucFirst(
itemNamePlural || pluralize(itemName)
);
const dataListLabel = i18n._(t`${itemDisplayName} List`);
const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `);
const emptyContentMessage = i18n._(
t`Please add ${itemDisplayNamePlural} to populate this list `
);
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
let Content;
if (hasContentLoading && items.length <= 0) {
Content = (<ContentLoading />);
Content = <ContentLoading />;
} else if (contentError) {
Content = (<ContentError error={contentError} />);
Content = <ContentError error={contentError} />;
} else if (items.length <= 0) {
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
Content = (
<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />
);
} else {
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
Content = (
<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>
);
}
if (items.length <= 0) {
@ -115,12 +132,16 @@ class PaginatedDataList extends React.Component {
itemCount={itemCount}
page={queryParams.page || 1}
perPage={queryParams.page_size}
perPageOptions={showPageSizeOptions ? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 }
] : []}
perPageOptions={
showPageSizeOptions
? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 },
]
: []
}
onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize}
/>
@ -142,11 +163,13 @@ PaginatedDataList.propTypes = {
itemNamePlural: PropTypes.string,
qsConfig: QSConfig.isRequired,
renderItem: PropTypes.func,
toolbarColumns: arrayOf(shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
})),
toolbarColumns: arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
})
),
showPageSizeOptions: PropTypes.bool,
renderToolbar: PropTypes.func,
hasContentLoading: PropTypes.bool,
@ -160,8 +183,8 @@ PaginatedDataList.defaultProps = {
itemName: 'item',
itemNamePlural: '',
showPageSizeOptions: true,
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
renderToolbar: (props) => (<DataListToolbar {...props} />),
renderItem: item => <PaginatedDataListItem key={item.id} item={item} />,
renderToolbar: props => <DataListToolbar {...props} />,
};
export { PaginatedDataList as _PaginatedDataList };

View File

@ -51,7 +51,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name',
}}
qsConfig={qsConfig}
/>, { context: { router: { history } } }
/>,
{ context: { router: { history } } }
);
const pagination = wrapper.find('Pagination');
@ -77,7 +78,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name',
}}
qsConfig={qsConfig}
/>, { context: { router: { history } } }
/>,
{ context: { router: { history } } }
);
const pagination = wrapper.find('Pagination');

View File

@ -10,11 +10,9 @@ import {
DropdownItem,
Form,
FormGroup,
TextInput as PFTextInput
TextInput as PFTextInput,
} from '@patternfly/react-core';
import {
SearchIcon
} from '@patternfly/react-icons';
import { SearchIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
@ -29,7 +27,8 @@ const Button = styled(PFButton)`
`;
const Dropdown = styled(PFDropdown)`
&&& { /* Higher specificity required because we are selecting unclassed elements */
&&& {
/* Higher specificity required because we are selecting unclassed elements */
> button {
min-height: 30px;
min-width: 70px;
@ -37,17 +36,19 @@ const Dropdown = styled(PFDropdown)`
padding: 0 10px;
margin: 0px;
> span { /* text element */
> span {
/* text element */
width: auto;
}
> svg { /* caret icon */
> svg {
/* caret icon */
margin: 0px;
padding-top: 3px;
padding-left: 3px;
}
}
}
}
`;
const NoOptionDropdown = styled.div`
@ -61,7 +62,7 @@ const InputFormGroup = styled(FormGroup)`
`;
class Search extends React.Component {
constructor (props) {
constructor(props) {
super(props);
const { sortedColumnKey } = this.props;
@ -77,11 +78,11 @@ class Search extends React.Component {
this.handleSearch = this.handleSearch.bind(this);
}
handleDropdownToggle (isSearchDropdownOpen) {
handleDropdownToggle(isSearchDropdownOpen) {
this.setState({ isSearchDropdownOpen });
}
handleDropdownSelect ({ target }) {
handleDropdownSelect({ target }) {
const { columns } = this.props;
const { innerText } = target;
@ -89,7 +90,7 @@ class Search extends React.Component {
this.setState({ isSearchDropdownOpen: false, searchKey });
}
handleSearch (e) {
handleSearch(e) {
// keeps page from fully reloading
e.preventDefault();
@ -102,22 +103,17 @@ class Search extends React.Component {
this.setState({ searchValue: '' });
}
handleSearchInputChange (searchValue) {
handleSearchInputChange(searchValue) {
this.setState({ searchValue });
}
render () {
render() {
const { up } = DropdownPosition;
const {
columns,
i18n
} = this.props;
const {
isSearchDropdownOpen,
searchKey,
searchValue,
} = this.state;
const { name: searchColumnName } = columns.find(({ key }) => key === searchKey);
const { columns, i18n } = this.props;
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
const { name: searchColumnName } = columns.find(
({ key }) => key === searchKey
);
const searchDropdownItems = columns
.filter(({ key, isSearchable }) => isSearchable && key !== searchKey)
@ -133,32 +129,38 @@ class Search extends React.Component {
{searchDropdownItems.length > 0 ? (
<FormGroup
fieldId="searchKeyDropdown"
label={(<span className="pf-screen-reader">{i18n._(t`Search key dropdown`)}</span>)}
label={
<span className="pf-screen-reader">
{i18n._(t`Search key dropdown`)}
</span>
}
>
<Dropdown
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
isOpen={isSearchDropdownOpen}
toggle={(
toggle={
<DropdownToggle
id="awx-search"
onToggle={this.handleDropdownToggle}
>
{searchColumnName}
</DropdownToggle>
)}
}
dropdownItems={searchDropdownItems}
/>
</FormGroup>
) : (
<NoOptionDropdown>
{searchColumnName}
</NoOptionDropdown>
<NoOptionDropdown>{searchColumnName}</NoOptionDropdown>
)}
<InputFormGroup
fieldId="searchValueTextInput"
label={(<span className="pf-screen-reader">{i18n._(t`Search value text input`)}</span>)}
label={
<span className="pf-screen-reader">
{i18n._(t`Search value text input`)}
</span>
}
>
<TextInput
type="search"
@ -190,7 +192,7 @@ Search.propTypes = {
Search.defaultProps = {
onSearch: null,
sortedColumnKey: 'name'
sortedColumnKey: 'name',
};
export default withI18n()(Search);

View File

@ -12,7 +12,9 @@ describe('<Search />', () => {
});
test('it triggers the expected callbacks', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
];
const searchBtn = 'button[aria-label="Search submit button"]';
const searchTextInput = 'input[aria-label="Search text input"]';
@ -20,11 +22,7 @@ describe('<Search />', () => {
const onSearch = jest.fn();
search = mountWithContexts(
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
);
search.find(searchTextInput).instance().value = 'test-321';
@ -36,14 +34,12 @@ describe('<Search />', () => {
});
test('handleDropdownToggle properly updates state', async () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
];
const onSearch = jest.fn();
const wrapper = mountWithContexts(
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
).find('Search');
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
wrapper.instance().handleDropdownToggle(true);
@ -53,18 +49,21 @@ describe('<Search />', () => {
test('handleDropdownSelect properly updates state', async () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
{ name: 'Description', key: 'description', isSortable: true, isSearchable: true }
{
name: 'Description',
key: 'description',
isSortable: true,
isSearchable: true,
},
];
const onSearch = jest.fn();
const wrapper = mountWithContexts(
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
).find('Search');
expect(wrapper.state('searchKey')).toEqual('name');
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } });
wrapper
.instance()
.handleDropdownSelect({ target: { innerText: 'Description' } });
expect(wrapper.state('searchKey')).toEqual('description');
});
});

View File

@ -12,7 +12,9 @@ describe('<Sort />', () => {
});
test('it triggers the expected callbacks', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true, isSearchable: true }];
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
];
const sortBtn = 'button[aria-label="Sort"]';
@ -38,7 +40,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -62,7 +64,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -86,7 +88,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -109,7 +111,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -133,8 +135,12 @@ describe('<Sort />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
const numericColumns = [
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
];
const onSort = jest.fn();
sort = mountWithContexts(

View File

@ -2,17 +2,13 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Card,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
import { UnifiedJobsAPI } from '@api';
import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar';
import PaginatedDataList, {
ToolbarDeleteButton
ToolbarDeleteButton,
} from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs';
@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('job', {
});
class JobList extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
hasContentLoading: true,
hasContentError: false,
contentError: null,
deletionError: false,
selected: [],
jobs: [],
@ -44,28 +40,28 @@ class JobList extends Component {
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
}
componentDidMount () {
componentDidMount() {
this.loadJobs();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.loadJobs();
}
}
handleDeleteErrorClose () {
handleDeleteErrorClose() {
this.setState({ deletionError: false });
}
handleSelectAll (isSelected) {
handleSelectAll(isSelected) {
const { jobs } = this.state;
const selected = isSelected ? [...jobs] : [];
this.setState({ selected });
}
handleSelect (item) {
handleSelect(item) {
const { selected } = this.state;
if (selected.some(s => s.id === item.id)) {
this.setState({ selected: selected.filter(s => s.id !== item.id) });
@ -74,7 +70,7 @@ class JobList extends Component {
}
}
async handleDelete () {
async handleDelete() {
const { selected } = this.state;
this.setState({ hasContentLoading: true, deletionError: false });
try {
@ -86,38 +82,37 @@ class JobList extends Component {
}
}
async loadJobs () {
async loadJobs() {
const { location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ hasContentError: false, hasContentLoading: true });
this.setState({ contentError: null, hasContentLoading: true });
try {
const { data: { count, results } } = await UnifiedJobsAPI.read(params);
const {
data: { count, results },
} = await UnifiedJobsAPI.read(params);
this.setState({
itemCount: count,
jobs: results,
selected: [],
});
} catch (err) {
this.setState({ hasContentError: true });
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
render () {
render() {
const {
hasContentError,
contentError,
hasContentLoading,
deletionError,
jobs,
itemCount,
selected,
} = this.state;
const {
match,
i18n
} = this.props;
const { match, i18n } = this.props;
const { medium } = PageSectionVariants;
const isAllSelected = selected.length === jobs.length;
const itemName = i18n._(t`Job`);
@ -125,17 +120,27 @@ class JobList extends Component {
<PageSection variant={medium}>
<Card>
<PaginatedDataList
hasContentError={hasContentError}
contentError={contentError}
hasContentLoading={hasContentLoading}
items={jobs}
itemCount={itemCount}
itemName={itemName}
qsConfig={QS_CONFIG}
toolbarColumns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
{ name: i18n._(t`Finished`), key: 'finished', isSortable: true, isNumeric: true },
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Finished`),
key: 'finished',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={(props) => (
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
@ -148,11 +153,11 @@ class JobList extends Component {
onDelete={this.handleDelete}
itemsToDelete={selected}
itemName={itemName}
/>
/>,
]}
/>
)}
renderItem={(job) => (
renderItem={job => (
<JobListItem
key={job.id}
value={job.name}

View File

@ -7,12 +7,10 @@ import { OrganizationsAPI, TeamsAPI, UsersAPI } from '@api';
import AddResourceRole from '@components/AddRole/AddResourceRole';
import AlertModal from '@components/AlertModal';
import DataListToolbar from '@components/DataListToolbar';
import PaginatedDataList, { ToolbarAddButton } from '@components/PaginatedDataList';
import {
getQSConfig,
encodeQueryString,
parseQueryString
} from '@util/qs';
import PaginatedDataList, {
ToolbarAddButton,
} from '@components/PaginatedDataList';
import { getQSConfig, encodeQueryString, parseQueryString } from '@util/qs';
import { Organization } from '@types';
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
@ -29,11 +27,11 @@ class OrganizationAccess extends React.Component {
organization: Organization.isRequired,
};
constructor (props) {
constructor(props) {
super(props);
this.state = {
accessRecords: [],
hasContentError: false,
contentError: null,
hasContentLoading: true,
hasDeletionError: false,
deletionRecord: null,
@ -51,11 +49,11 @@ class OrganizationAccess extends React.Component {
this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
}
componentDidMount () {
componentDidMount() {
this.loadAccessList();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
const prevParams = parseQueryString(QS_CONFIG, prevProps.location.search);
@ -66,43 +64,40 @@ class OrganizationAccess extends React.Component {
}
}
async loadAccessList () {
async loadAccessList() {
const { organization, location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ hasContentError: false, hasContentLoading: true });
this.setState({ contentError: null, hasContentLoading: true });
try {
const {
data: {
results: accessRecords = [],
count: itemCount = 0
}
data: { results: accessRecords = [], count: itemCount = 0 },
} = await OrganizationsAPI.readAccessList(organization.id, params);
this.setState({ itemCount, accessRecords });
} catch (error) {
this.setState({ hasContentError: true });
} catch (err) {
this.setState({ cotentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
handleDeleteOpen (deletionRole, deletionRecord) {
handleDeleteOpen(deletionRole, deletionRecord) {
this.setState({ deletionRole, deletionRecord });
}
handleDeleteCancel () {
handleDeleteCancel() {
this.setState({ deletionRole: null, deletionRecord: null });
}
handleDeleteErrorClose () {
handleDeleteErrorClose() {
this.setState({
hasDeletionError: false,
deletionRecord: null,
deletionRole: null
deletionRole: null,
});
}
async handleDeleteConfirm () {
async handleDeleteConfirm() {
const { deletionRole, deletionRecord } = this.state;
if (!deletionRole || !deletionRecord) {
@ -111,7 +106,10 @@ class OrganizationAccess extends React.Component {
let promise;
if (typeof deletionRole.team_id !== 'undefined') {
promise = TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
promise = TeamsAPI.disassociateRole(
deletionRole.team_id,
deletionRole.id
);
} else {
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
}
@ -121,34 +119,34 @@ class OrganizationAccess extends React.Component {
await promise.then(this.loadAccessList);
this.setState({
deletionRole: null,
deletionRecord: null
deletionRecord: null,
});
} catch (error) {
this.setState({
hasContentLoading: false,
hasDeletionError: true
hasDeletionError: true,
});
}
}
handleAddClose () {
handleAddClose() {
this.setState({ isAddModalOpen: false });
}
handleAddOpen () {
handleAddOpen() {
this.setState({ isAddModalOpen: true });
}
handleAddSuccess () {
handleAddSuccess() {
this.setState({ isAddModalOpen: false });
this.loadAccessList();
}
render () {
render() {
const { organization, i18n } = this.props;
const {
accessRecords,
hasContentError,
contentError,
hasContentLoading,
deletionRole,
deletionRecord,
@ -157,28 +155,51 @@ class OrganizationAccess extends React.Component {
isAddModalOpen,
} = this.state;
const canEdit = organization.summary_fields.user_capabilities.edit;
const isDeleteModalOpen = !hasContentLoading && !hasDeletionError && deletionRole;
const isDeleteModalOpen =
!hasContentLoading && !hasDeletionError && deletionRole;
return (
<Fragment>
<PaginatedDataList
hasContentError={hasContentError}
error={contentError}
hasContentLoading={hasContentLoading}
items={accessRecords}
itemCount={itemCount}
itemName="role"
qsConfig={QS_CONFIG}
toolbarColumns={[
{ name: i18n._(t`First Name`), key: 'first_name', isSortable: true, isSearchable: true },
{ name: i18n._(t`Username`), key: 'username', isSortable: true, isSearchable: true },
{ name: i18n._(t`Last Name`), key: 'last_name', isSortable: true, isSearchable: true },
{
name: i18n._(t`First Name`),
key: 'first_name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Username`),
key: 'username',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
isSortable: true,
isSearchable: true,
},
]}
renderToolbar={(props) => (
renderToolbar={props => (
<DataListToolbar
{...props}
additionalControls={canEdit ? [
<ToolbarAddButton key="add" onClick={this.handleAddOpen} />
] : null}
additionalControls={
canEdit
? [
<ToolbarAddButton
key="add"
onClick={this.handleAddOpen}
/>,
]
: null
}
/>
)}
renderItem={accessRecord => (

View File

@ -103,7 +103,7 @@ describe('<OrganizationAccess />', () => {
expect(wrapper.find('OrganizationAccess').state('hasContentLoading')).toBe(
false
);
expect(wrapper.find('OrganizationAccess').state('hasContentError')).toBe(false);
expect(wrapper.find('OrganizationAccess').state('contentError')).toBe(null);
done();
});

View File

@ -34,7 +34,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
}
>
<WithI18n
hasContentError={false}
error={null}
hasContentLoading={true}
itemCount={0}
itemName="role"
@ -83,7 +83,7 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
withHash={true}
>
<withRouter(PaginatedDataList)
hasContentError={false}
error={null}
hasContentLoading={true}
i18n={"/i18n/"}
itemCount={0}
@ -130,7 +130,8 @@ exports[`<OrganizationAccess /> initially renders succesfully 1`] = `
>
<Route>
<PaginatedDataList
hasContentError={false}
contentError={null}
error={null}
hasContentLoading={true}
history={"/history/"}
i18n={"/i18n/"}

View File

@ -2,11 +2,7 @@ import React, { Component, Fragment } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Card,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
import { OrganizationsAPI } from '@api';
import AlertModal from '@components/AlertModal';
@ -26,12 +22,12 @@ const QS_CONFIG = getQSConfig('organization', {
});
class OrganizationsList extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
hasContentLoading: true,
hasContentError: false,
contentError: null,
hasDeletionError: false,
organizations: [],
selected: [],
@ -46,25 +42,25 @@ class OrganizationsList extends Component {
this.loadOrganizations = this.loadOrganizations.bind(this);
}
componentDidMount () {
componentDidMount() {
this.loadOrganizations();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.loadOrganizations();
}
}
handleSelectAll (isSelected) {
handleSelectAll(isSelected) {
const { organizations } = this.state;
const selected = isSelected ? [...organizations] : [];
this.setState({ selected });
}
handleSelect (row) {
handleSelect(row) {
const { selected } = this.state;
if (selected.some(s => s.id === row.id)) {
@ -74,16 +70,16 @@ class OrganizationsList extends Component {
}
}
handleDeleteErrorClose () {
handleDeleteErrorClose() {
this.setState({ hasDeletionError: false });
}
async handleOrgDelete () {
async handleOrgDelete() {
const { selected } = this.state;
this.setState({ hasContentLoading: true, hasDeletionError: false });
try {
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
await Promise.all(selected.map(org => OrganizationsAPI.destroy(org.id)));
} catch (err) {
this.setState({ hasDeletionError: true });
} finally {
@ -91,7 +87,7 @@ class OrganizationsList extends Component {
}
}
async loadOrganizations () {
async loadOrganizations() {
const { location } = this.props;
const { actions: cachedActions } = this.state;
const params = parseQueryString(QS_CONFIG, location.search);
@ -108,9 +104,16 @@ class OrganizationsList extends Component {
optionsPromise,
]);
this.setState({ hasContentError: false, hasContentLoading: true });
this.setState({ contentError: null, hasContentLoading: true });
try {
const [{ data: { count, results } }, { data: { actions } }] = await promises;
const [
{
data: { count, results },
},
{
data: { actions },
},
] = await promises;
this.setState({
actions,
itemCount: count,
@ -118,20 +121,18 @@ class OrganizationsList extends Component {
selected: [],
});
} catch (err) {
this.setState(({ hasContentError: true }));
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
render () {
const {
medium,
} = PageSectionVariants;
render() {
const { medium } = PageSectionVariants;
const {
actions,
itemCount,
hasContentError,
contentError,
hasContentLoading,
hasDeletionError,
selected,
@ -139,7 +140,8 @@ class OrganizationsList extends Component {
} = this.state;
const { match, i18n } = this.props;
const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const isAllSelected = selected.length === organizations.length;
return (
@ -147,18 +149,33 @@ class OrganizationsList extends Component {
<PageSection variant={medium}>
<Card>
<PaginatedDataList
hasContentError={hasContentError}
error={contentError}
hasContentLoading={hasContentLoading}
items={organizations}
itemCount={itemCount}
itemName="organization"
qsConfig={QS_CONFIG}
toolbarColumns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true },
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true },
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={(props) => (
renderToolbar={props => (
<DataListToolbar
{...props}
showSelectAll
@ -171,13 +188,13 @@ class OrganizationsList extends Component {
itemsToDelete={selected}
itemName="Organization"
/>,
canAdd
? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
: null,
canAdd ? (
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
) : null,
]}
/>
)}
renderItem={(o) => (
renderItem={o => (
<OrganizationListItem
key={o.id}
organization={o}
@ -187,8 +204,9 @@ class OrganizationsList extends Component {
/>
)}
emptyStateControls={
canAdd ? <ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
: null
canAdd ? (
<ToolbarAddButton key="add" linkTo={`${match.url}/add`} />
) : null
}
/>
</Card>

View File

@ -24,7 +24,7 @@ const COLUMNS = [
];
class OrganizationNotifications extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
contentError: null,
@ -38,22 +38,24 @@ class OrganizationNotifications extends Component {
typeLabels: null,
};
this.handleNotificationToggle = this.handleNotificationToggle.bind(this);
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(this);
this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(
this
);
this.loadNotifications = this.loadNotifications.bind(this);
}
componentDidMount () {
componentDidMount() {
this.loadNotifications();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.loadNotifications();
}
}
async loadNotifications () {
async loadNotifications() {
const { id, location } = this.props;
const { typeLabels } = this.state;
const params = parseQueryString(QS_CONFIG, location.search);
@ -67,13 +69,13 @@ class OrganizationNotifications extends Component {
this.setState({ contentError: null, hasContentLoading: true });
try {
const {
data: {
count: itemCount = 0,
results: notifications = [],
}
data: { count: itemCount = 0, results: notifications = [] },
} = await OrganizationsAPI.readNotificationTemplates(id, params);
const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(id, params);
const optionsResponse = await OrganizationsAPI.readOptionsNotificationTemplates(
id,
params
);
let idMatchParams;
if (notifications.length > 0) {
@ -122,7 +124,7 @@ class OrganizationNotifications extends Component {
}
}
async handleNotificationToggle (notificationId, isCurrentlyOn, status) {
async handleNotificationToggle(notificationId, isCurrentlyOn, status) {
const { id } = this.props;
let stateArrayName;
@ -135,13 +137,15 @@ class OrganizationNotifications extends Component {
let stateUpdateFunction;
if (isCurrentlyOn) {
// when switching off, remove the toggled notification id from the array
stateUpdateFunction = (prevState) => ({
[stateArrayName]: prevState[stateArrayName].filter(i => i !== notificationId)
stateUpdateFunction = prevState => ({
[stateArrayName]: prevState[stateArrayName].filter(
i => i !== notificationId
),
});
} else {
// when switching on, add the toggled notification id to the array
stateUpdateFunction = (prevState) => ({
[stateArrayName]: prevState[stateArrayName].concat(notificationId)
stateUpdateFunction = prevState => ({
[stateArrayName]: prevState[stateArrayName].concat(notificationId),
});
}
@ -161,11 +165,11 @@ class OrganizationNotifications extends Component {
}
}
handleNotificationErrorClose () {
handleNotificationErrorClose() {
this.setState({ toggleError: false });
}
render () {
render() {
const { canToggleNotifications, i18n } = this.props;
const {
contentError,
@ -176,7 +180,7 @@ class OrganizationNotifications extends Component {
notifications,
successTemplateIds,
errorTemplateIds,
typeLabels
typeLabels,
} = this.state;
return (
@ -189,7 +193,7 @@ class OrganizationNotifications extends Component {
itemName="notification"
qsConfig={QS_CONFIG}
toolbarColumns={COLUMNS}
renderItem={(notification) => (
renderItem={notification => (
<NotificationListItem
key={notification.id}
notification={notification}

View File

@ -164,7 +164,6 @@ exports[`<OrganizationNotifications /> initially renders succesfully 1`] = `
<Route>
<PaginatedDataList
contentError={null}
hasContentError={false}
hasContentLoading={false}
history={"/history/"}
i18n={"/i18n/"}

View File

@ -8,30 +8,24 @@ import { QuestionCircleIcon } from '@patternfly/react-icons';
import { InstanceGroupsAPI } from '@api';
import Lookup from '@components/Lookup';
const getInstanceGroups = async (params) => InstanceGroupsAPI.read(params);
const getInstanceGroups = async params => InstanceGroupsAPI.read(params);
class InstanceGroupsLookup extends React.Component {
render () {
render() {
const { value, tooltip, onChange, i18n } = this.props;
return (
<FormGroup
label={(
label={
<Fragment>
{i18n._(t`Instance Groups`)}
{' '}
{
tooltip && (
<Tooltip
position="right"
content={tooltip}
>
<QuestionCircleIcon />
</Tooltip>
)
}
{i18n._(t`Instance Groups`)}{' '}
{tooltip && (
<Tooltip position="right" content={tooltip}>
<QuestionCircleIcon />
</Tooltip>
)}
</Fragment>
)}
}
fieldId="org-instance-groups"
>
<Lookup
@ -43,9 +37,24 @@ class InstanceGroupsLookup extends React.Component {
getItems={getInstanceGroups}
multiple
columns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
{ name: i18n._(t`Modified`), key: 'modified', isSortable: false, isNumeric: true },
{ name: i18n._(t`Created`), key: 'created', isSortable: false, isNumeric: true }
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: false,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: false,
isNumeric: true,
},
]}
sortedColumnKey="name"
/>

View File

@ -2,21 +2,17 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Card,
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
import {
JobTemplatesAPI,
UnifiedJobTemplatesAPI,
WorkflowJobTemplatesAPI
WorkflowJobTemplatesAPI,
} from '@api';
import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar';
import PaginatedDataList, {
ToolbarDeleteButton
ToolbarDeleteButton,
} from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs';
@ -28,16 +24,16 @@ const QS_CONFIG = getQSConfig('template', {
page: 1,
page_size: 5,
order_by: 'name',
type: 'job_template,workflow_job_template'
type: 'job_template,workflow_job_template',
});
class TemplatesList extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
hasContentLoading: true,
hasContentError: false,
contentError: null,
hasDeletionError: false,
selected: [],
templates: [],
@ -50,28 +46,28 @@ class TemplatesList extends Component {
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
}
componentDidMount () {
componentDidMount() {
this.loadTemplates();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.loadTemplates();
}
}
handleDeleteErrorClose () {
handleDeleteErrorClose() {
this.setState({ hasDeletionError: false });
}
handleSelectAll (isSelected) {
handleSelectAll(isSelected) {
const { templates } = this.state;
const selected = isSelected ? [...templates] : [];
this.setState({ selected });
}
handleSelect (template) {
handleSelect(template) {
const { selected } = this.state;
if (selected.some(s => s.id === template.id)) {
this.setState({ selected: selected.filter(s => s.id !== template.id) });
@ -80,20 +76,22 @@ class TemplatesList extends Component {
}
}
async handleTemplateDelete () {
async handleTemplateDelete() {
const { selected } = this.state;
this.setState({ hasContentLoading: true, hasDeletionError: false });
try {
await Promise.all(selected.map(({ type, id }) => {
let deletePromise;
if (type === 'job_template') {
deletePromise = JobTemplatesAPI.destroy(id);
} else if (type === 'workflow_job_template') {
deletePromise = WorkflowJobTemplatesAPI.destroy(id);
}
return deletePromise;
}));
await Promise.all(
selected.map(({ type, id }) => {
let deletePromise;
if (type === 'job_template') {
deletePromise = JobTemplatesAPI.destroy(id);
} else if (type === 'workflow_job_template') {
deletePromise = WorkflowJobTemplatesAPI.destroy(id);
}
return deletePromise;
})
);
} catch (err) {
this.setState({ hasDeletionError: true });
} finally {
@ -101,56 +99,70 @@ class TemplatesList extends Component {
}
}
async loadTemplates () {
async loadTemplates() {
const { location } = this.props;
const params = parseQueryString(QS_CONFIG, location.search);
this.setState({ hasContentError: false, hasContentLoading: true });
this.setState({ contentError: null, hasContentLoading: true });
try {
const { data: { count, results } } = await UnifiedJobTemplatesAPI.read(params);
const {
data: { count, results },
} = await UnifiedJobTemplatesAPI.read(params);
this.setState({
itemCount: count,
templates: results,
selected: [],
});
} catch (err) {
this.setState({ hasContentError: true });
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
render () {
render() {
const {
hasContentError,
contentError,
hasContentLoading,
hasDeletionError,
templates,
itemCount,
selected,
} = this.state;
const {
match,
i18n
} = this.props;
const { match, i18n } = this.props;
const isAllSelected = selected.length === templates.length;
const { medium } = PageSectionVariants;
return (
<PageSection variant={medium}>
<Card>
<PaginatedDataList
hasContentError={hasContentError}
err={contentError}
hasContentLoading={hasContentLoading}
items={templates}
itemCount={itemCount}
itemName={i18n._(t`Template`)}
qsConfig={QS_CONFIG}
toolbarColumns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true, isSearchable: true },
{ name: i18n._(t`Modified`), key: 'modified', isSortable: true, isNumeric: true },
{ name: i18n._(t`Created`), key: 'created', isSortable: true, isNumeric: true },
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={(props) => (
renderToolbar={props => (
<DatalistToolbar
{...props}
showSelectAll
@ -163,11 +175,11 @@ class TemplatesList extends Component {
onDelete={this.handleTemplateDelete}
itemsToDelete={selected}
itemName={i18n._(t`Template`)}
/>
/>,
]}
/>
)}
renderItem={(template) => (
renderItem={template => (
<TemplateListItem
key={template.id}
value={template.name}

View File

@ -4,16 +4,17 @@
* @param {array} array in the format [ [ key, value ], ...]
* @return {object} object in the forms { key: value, ... }
*/
const toObject = (entriesArr) => entriesArr.reduce((acc, [key, value]) => {
if (acc[key] && Array.isArray(acc[key])) {
acc[key].push(value);
} else if (acc[key]) {
acc[key] = [acc[key], value];
} else {
acc[key] = value;
}
return acc;
}, {});
const toObject = entriesArr =>
entriesArr.reduce((acc, [key, value]) => {
if (acc[key] && Array.isArray(acc[key])) {
acc[key].push(value);
} else if (acc[key]) {
acc[key] = [acc[key], value];
} else {
acc[key] = value;
}
return acc;
}, {});
/**
* helper function to namespace params object
@ -30,7 +31,7 @@ const namespaceParams = (namespace, params = {}) => {
});
return namespaced || {};
}
};
/**
* helper function to remove namespace from params object
@ -47,7 +48,7 @@ const denamespaceParams = (namespace, params = {}) => {
});
return denamespaced || {};
}
};
/**
* helper function to check the namespace of a param is what you expec
@ -59,7 +60,7 @@ const namespaceMatches = (namespace, fieldname) => {
if (!namespace) return !fieldname.includes('.');
return fieldname.startsWith(`${namespace}.`);
}
};
/**
* helper function to check the value of a param is equal to another
@ -72,13 +73,15 @@ const paramValueIsEqual = (one, two) => {
if (Array.isArray(one) && Array.isArray(two)) {
isEqual = one.filter(val => two.indexOf(val) > -1).length === 0;
} else if ((typeof(one) === "string" && typeof(two) === "string") ||
(typeof(one) === "number" && typeof(two) === "number")){
} else if (
(typeof one === 'string' && typeof two === 'string') ||
(typeof one === 'number' && typeof two === 'number')
) {
isEqual = one === two;
}
return isEqual;
}
};
/**
* Convert query param object to url query string
@ -87,17 +90,19 @@ const paramValueIsEqual = (one, two) => {
* @param {object} query param object
* @return {string} url query string
*/
export const encodeQueryString = (params) => {
export const encodeQueryString = params => {
if (!params) return '';
return Object.keys(params)
.sort()
.filter(key => params[key] !== null)
.map(key => ([key, params[key]]))
.map(key => [key, params[key]])
.map(([key, value]) => {
// if value is array, should return more than one key value pair
if (Array.isArray(value)) {
return value.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
return value
.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join('&');
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
@ -115,22 +120,31 @@ export const encodeNonDefaultQueryString = (config, params) => {
if (!params) return '';
const namespacedParams = namespaceParams(config.namespace, params);
const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams);
const namespacedDefaults = namespaceParams(
config.namespace,
config.defaultParams
);
const namespacedDefaultKeys = Object.keys(namespacedDefaults);
const namespacedParamsWithoutDefaultsKeys = Object.keys(namespacedParams)
.filter(key => namespacedDefaultKeys.indexOf(key) === -1 ||
!paramValueIsEqual(namespacedParams[key], namespacedDefaults[key]));
const namespacedParamsWithoutDefaultsKeys = Object.keys(
namespacedParams
).filter(
key =>
namespacedDefaultKeys.indexOf(key) === -1 ||
!paramValueIsEqual(namespacedParams[key], namespacedDefaults[key])
);
return namespacedParamsWithoutDefaultsKeys
.sort()
.filter(key => namespacedParams[key] !== null)
.map(key => {
return ([key, namespacedParams[key]])
return [key, namespacedParams[key]];
})
.map(([key, value]) => {
// if value is array, should return more than one key value pair
if (Array.isArray(value)) {
return value.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
return value
.map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join('&');
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
@ -144,7 +158,7 @@ export const encodeNonDefaultQueryString = (config, params) => {
* @param {array} params that are number fields
* @return {object} query param object
*/
export function getQSConfig (
export function getQSConfig(
namespace,
defaultParams = { page: 1, page_size: 5, order_by: 'name' },
integerFields = ['page', 'page_size']
@ -165,12 +179,16 @@ export function getQSConfig (
* @param {string} url query string
* @return {object} query param object
*/
export function parseQueryString (config, queryString) {
export function parseQueryString(config, queryString) {
if (!queryString) return config.defaultParams;
const namespacedIntegerFields = config.integerFields.map(f => config.namespace ? `${config.namespace}.${f}` : f);
const namespacedIntegerFields = config.integerFields.map(f =>
config.namespace ? `${config.namespace}.${f}` : f
);
const keyValuePairs = queryString.replace(/^\?/, '').split('&')
const keyValuePairs = queryString
.replace(/^\?/, '')
.split('&')
.map(s => s.split('='))
.map(([key, value]) => {
if (namespacedIntegerFields.includes(key)) {
@ -185,37 +203,38 @@ export function parseQueryString (config, queryString) {
// needs to return array for duplicate keys
// ie [[k1, v1], [k1, v2], [k2, v3]]
// -> [[k1, [v1, v2]], [k2, v3]]
const dedupedKeyValuePairs = Object.keys(keyValueObject)
.map(key => {
const values = keyValuePairs
.filter(([k]) => k === key)
.map(([, v]) => v);
const dedupedKeyValuePairs = Object.keys(keyValueObject).map(key => {
const values = keyValuePairs.filter(([k]) => k === key).map(([, v]) => v);
if (values.length === 1) {
return [key, values[0]];
}
if (values.length === 1) {
return [key, values[0]];
}
return [key, values];
});
return [key, values];
});
const parsed = Object.assign(...dedupedKeyValuePairs.map(([k, v]) => ({
[k]: v
})));
const parsed = Object.assign(
...dedupedKeyValuePairs.map(([k, v]) => ({
[k]: v,
}))
);
const namespacedParams = {};
Object.keys(parsed)
.forEach(field => {
if (namespaceMatches(config.namespace, field)) {
let fieldname = field;
if (config.namespace) {
fieldname = field.substr(config.namespace.length + 1);
}
namespacedParams[fieldname] = parsed[field];
Object.keys(parsed).forEach(field => {
if (namespaceMatches(config.namespace, field)) {
let fieldname = field;
if (config.namespace) {
fieldname = field.substr(config.namespace.length + 1);
}
});
namespacedParams[fieldname] = parsed[field];
}
});
const namespacedDefaults = namespaceParams(config.namespace, config.defaultParams);
const namespacedDefaults = namespaceParams(
config.namespace,
config.defaultParams
);
Object.keys(namespacedDefaults)
.filter(key => Object.keys(parsed).indexOf(key) === -1)
@ -238,11 +257,12 @@ export function parseQueryString (config, queryString) {
* @param {object} namespaced params object of default params
* @return {object} namespaced params object of only defaults
*/
const getDefaultParams = (params, defaults) => toObject(
Object.keys(params)
.filter(key => Object.keys(defaults).indexOf(key) > -1)
.map(key => [key, params[key]])
);
const getDefaultParams = (params, defaults) =>
toObject(
Object.keys(params)
.filter(key => Object.keys(defaults).indexOf(key) > -1)
.map(key => [key, params[key]])
);
/**
* helper function to get params that are not defaults
@ -250,11 +270,12 @@ const getDefaultParams = (params, defaults) => toObject(
* @param {object} namespaced params object of default params
* @return {object} namespaced params object of non-defaults
*/
const getNonDefaultParams = (params, defaults) => toObject(
Object.keys(params)
.filter(key => Object.keys(defaults).indexOf(key) === -1)
.map(key => [key, params[key]])
);
const getNonDefaultParams = (params, defaults) =>
toObject(
Object.keys(params)
.filter(key => Object.keys(defaults).indexOf(key) === -1)
.map(key => [key, params[key]])
);
/**
* helper function to merge old and new params together
@ -262,9 +283,9 @@ const getNonDefaultParams = (params, defaults) => toObject(
* @param {object} namespaced params object of new params
* @return {object} merged namespaced params object
*/
const getMergedParams = (oldParams, newParams) => toObject(
Object.keys(oldParams)
.map(key => {
const getMergedParams = (oldParams, newParams) =>
toObject(
Object.keys(oldParams).map(key => {
let oldVal = oldParams[key];
const newVal = newParams[key];
if (newVal) {
@ -276,7 +297,7 @@ const getMergedParams = (oldParams, newParams) => toObject(
}
return [key, oldVal];
})
);
);
/**
* helper function to get new params that are not in merged params
@ -284,11 +305,12 @@ const getMergedParams = (oldParams, newParams) => toObject(
* @param {object} namespaced params object of new params
* @return {object} remaining new namespaced params object
*/
const getRemainingNewParams = (mergedParams, newParams) => toObject(
Object.keys(newParams)
.filter(key => Object.keys(mergedParams).indexOf(key) === -1)
.map(key => [key, newParams[key]])
);
const getRemainingNewParams = (mergedParams, newParams) =>
toObject(
Object.keys(newParams)
.filter(key => Object.keys(mergedParams).indexOf(key) === -1)
.map(key => [key, newParams[key]])
);
/**
* Merges existing params of search string with new ones and returns the updated list of params
@ -297,13 +319,16 @@ const getRemainingNewParams = (mergedParams, newParams) => toObject(
* @param {object} object with new params to add
* @return {object} query param object
*/
export function addParams (config, searchString, newParams) {
export function addParams(config, searchString, newParams) {
const namespacedOldParams = namespaceParams(
config.namespace,
parseQueryString(config, searchString)
);
const namespacedNewParams = namespaceParams(config.namespace, newParams);
const namespacedDefaultParams = namespaceParams(config.namespace, config.defaultParams);
const namespacedDefaultParams = namespaceParams(
config.namespace,
config.defaultParams
);
const namespacedOldParamsNotDefaults = getNonDefaultParams(
namespacedOldParams,
@ -320,7 +345,7 @@ export function addParams (config, searchString, newParams) {
return denamespaceParams(config.namespace, {
...getDefaultParams(namespacedOldParams, namespacedDefaultParams),
...namespacedMergedParams,
...getRemainingNewParams(namespacedMergedParams, namespacedNewParams)
...getRemainingNewParams(namespacedMergedParams, namespacedNewParams),
});
}
@ -331,26 +356,29 @@ export function addParams (config, searchString, newParams) {
* @param {object} object with new params to remove
* @return {object} query param object
*/
export function removeParams (config, searchString, paramsToRemove) {
export function removeParams(config, searchString, paramsToRemove) {
const oldParams = parseQueryString(config, searchString);
const paramsEntries = [];
Object.entries(oldParams)
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(val => {
paramsEntries.push([key, val]);
})
} else {
paramsEntries.push([key, value]);
}
})
Object.entries(oldParams).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(val => {
paramsEntries.push([key, val]);
});
} else {
paramsEntries.push([key, value]);
}
});
const paramsToRemoveEntries = Object.entries(paramsToRemove);
const remainingEntries = paramsEntries
.filter(([key, value]) => paramsToRemoveEntries
.filter(([newKey, newValue]) => key === newKey && value === newValue).length === 0);
const remainingEntries = paramsEntries.filter(
([key, value]) =>
paramsToRemoveEntries.filter(
([newKey, newValue]) => key === newKey && value === newValue
).length === 0
);
const remainingObject = toObject(remainingEntries);
const defaultEntriesLeftover = Object.entries(config.defaultParams)
.filter(([key]) => !remainingObject[key]);
const defaultEntriesLeftover = Object.entries(config.defaultParams).filter(
([key]) => !remainingObject[key]
);
const finalParamsEntries = remainingEntries;
defaultEntriesLeftover.forEach(value => {
finalParamsEntries.push(value);

View File

@ -4,7 +4,7 @@ import {
parseQueryString,
getQSConfig,
addParams,
removeParams
removeParams,
} from './qs';
describe('qs (qs.js)', () => {
@ -13,14 +13,19 @@ describe('qs (qs.js)', () => {
[
[null, ''],
[{}, ''],
[{ order_by: 'name', page: 1, page_size: 5 }, 'order_by=name&page=1&page_size=5'],
[{ '-order_by': 'name', page: '1', page_size: 5 }, '-order_by=name&page=1&page_size=5'],
]
.forEach(([params, expectedQueryString]) => {
const actualQueryString = encodeQueryString(params);
[
{ order_by: 'name', page: 1, page_size: 5 },
'order_by=name&page=1&page_size=5',
],
[
{ '-order_by': 'name', page: '1', page_size: 5 },
'-order_by=name&page=1&page_size=5',
],
].forEach(([params, expectedQueryString]) => {
const actualQueryString = encodeQueryString(params);
expect(actualQueryString).toEqual(expectedQueryString);
});
expect(actualQueryString).toEqual(expectedQueryString);
});
});
test('encodeQueryString omits null values', () => {
@ -35,7 +40,7 @@ describe('qs (qs.js)', () => {
describe('encodeNonDefaultQueryString', () => {
const config = {
namespace: null,
defaultParams: { page: 1, page_size: 5, order_by: 'name'},
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page'],
};
@ -45,14 +50,19 @@ describe('qs (qs.js)', () => {
[{}, ''],
[{ order_by: 'name', page: 1, page_size: 5 }, ''],
[{ order_by: '-name', page: 1, page_size: 5 }, 'order_by=-name'],
[{ order_by: '-name', page: 3, page_size: 10 }, 'order_by=-name&page=3&page_size=10'],
[{ order_by: '-name', page: 3, page_size: 10, foo: 'bar' }, 'foo=bar&order_by=-name&page=3&page_size=10'],
]
.forEach(([params, expectedQueryString]) => {
const actualQueryString = encodeNonDefaultQueryString(config, params);
[
{ order_by: '-name', page: 3, page_size: 10 },
'order_by=-name&page=3&page_size=10',
],
[
{ order_by: '-name', page: 3, page_size: 10, foo: 'bar' },
'foo=bar&order_by=-name&page=3&page_size=10',
],
].forEach(([params, expectedQueryString]) => {
const actualQueryString = encodeNonDefaultQueryString(config, params);
expect(actualQueryString).toEqual(expectedQueryString);
});
expect(actualQueryString).toEqual(expectedQueryString);
});
});
test('encodeNonDefaultQueryString omits null values', () => {
@ -114,7 +124,7 @@ describe('qs (qs.js)', () => {
const query = '';
expect(parseQueryString(config, query)).toEqual({
page: 1,
page_size: 15
page_size: 15,
});
});
@ -222,7 +232,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&page=3';
const newParams = { bag: 'boom' }
const newParams = { bag: 'boom' };
expect(addParams(config, query, newParams)).toEqual({
baz: 'bar',
bag: 'boom',
@ -238,7 +248,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&page=3';
const newParams = { baz: 'boom' }
const newParams = { baz: 'boom' };
expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'boom'],
page: 3,
@ -253,7 +263,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&page=3';
const newParams = { page: 5 }
const newParams = { page: 5 };
expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'],
page: 5,
@ -268,7 +278,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&page=3';
const newParams = { baz: 'bust', pat: 'pal' }
const newParams = { baz: 'bust', pat: 'pal' };
expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'bust'],
pat: 'pal',
@ -284,7 +294,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.page=3';
const newParams = { bag: 'boom' }
const newParams = { bag: 'boom' };
expect(addParams(config, query, newParams)).toEqual({
baz: 'bar',
bag: 'boom',
@ -300,7 +310,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&foo.page=3';
const newParams = { bag: 'boom' }
const newParams = { bag: 'boom' };
expect(addParams(config, query, newParams)).toEqual({
baz: 'bar',
bag: 'boom',
@ -316,7 +326,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.page=3';
const newParams = { baz: 'boom' }
const newParams = { baz: 'boom' };
expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'boom'],
page: 3,
@ -331,7 +341,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.page=3';
const newParams = { page: 5 }
const newParams = { page: 5 };
expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'],
page: 5,
@ -346,7 +356,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.page=3';
const newParams = { baz: 'bust', pat: 'pal' }
const newParams = { baz: 'bust', pat: 'pal' };
expect(addParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang', 'bust'],
pat: 'pal',
@ -364,7 +374,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&page=3&bag=boom';
const newParams = { bag: 'boom' }
const newParams = { bag: 'boom' };
expect(removeParams(config, query, newParams)).toEqual({
baz: 'bar',
page: 3,
@ -379,7 +389,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&page=3';
const newParams = { baz: 'bar' }
const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({
baz: 'bang',
page: 3,
@ -394,7 +404,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&baz=bust&page=3';
const newParams = { baz: 'bar' }
const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({
baz: ['bang', 'bust'],
page: 3,
@ -409,7 +419,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&page=3';
const newParams = { page: 3 }
const newParams = { page: 3 };
expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'],
page: 1,
@ -424,7 +434,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?baz=bar&baz=bang&baz=bust&pat=pal&page=3';
const newParams = { baz: 'bust', pat: 'pal' }
const newParams = { baz: 'bust', pat: 'pal' };
expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'],
page: 3,
@ -439,7 +449,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.page=3';
const newParams = { baz: 'bar' }
const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({
page: 3,
page_size: 15,
@ -453,7 +463,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&foo.page=3';
const newParams = { baz: 'bar' }
const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({
page: 1,
page_size: 15,
@ -467,7 +477,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.page=3';
const newParams = { baz: 'bar' }
const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({
baz: 'bang',
page: 3,
@ -482,7 +492,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.page=3';
const newParams = { baz: 'bar' }
const newParams = { baz: 'bar' };
expect(removeParams(config, query, newParams)).toEqual({
baz: ['bang', 'bust'],
page: 3,
@ -497,7 +507,7 @@ describe('qs (qs.js)', () => {
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.page=3';
const newParams = { page: 3 }
const newParams = { page: 3 };
expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'],
page: 1,
@ -511,8 +521,9 @@ describe('qs (qs.js)', () => {
defaultParams: { page: 1, page_size: 15 },
integerFields: ['page', 'page_size'],
};
const query = '?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3';
const newParams = { baz: 'bust', pat: 'pal' }
const query =
'?item.baz=bar&item.baz=bang&item.baz=bust&item.pat=pal&item.page=3';
const newParams = { baz: 'bust', pat: 'pal' };
expect(removeParams(config, query, newParams)).toEqual({
baz: ['bar', 'bang'],
page: 3,